##// END OF EJS Templates
adjustments to notebook app logging...
MinRK -
Show More
@@ -0,0 +1,52 b''
1 #-----------------------------------------------------------------------------
2 # Copyright (C) 2013 The IPython Development Team
3 #
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING, distributed as part of this software.
6 #-----------------------------------------------------------------------------
7
8 import json
9 from tornado.log import access_log
10
11 def log_request(handler):
12 """log a bit more information about each request than tornado's default
13
14 - move static file get success to debug-level (reduces noise)
15 - get proxied IP instead of proxy IP
16 - log referer for redirect and failed requests
17 - log user-agent for failed requests
18 """
19 status = handler.get_status()
20 request = handler.request
21 if status < 300 or status == 304:
22 # Successes (or 304 FOUND) are debug-level
23 log_method = access_log.debug
24 elif status < 400:
25 log_method = access_log.info
26 elif status < 500:
27 log_method = access_log.warning
28 else:
29 log_method = access_log.error
30
31 request_time = 1000.0 * handler.request.request_time()
32 ns = dict(
33 status=status,
34 method=request.method,
35 ip=request.remote_ip,
36 uri=request.uri,
37 request_time=request_time,
38 )
39 msg = "{status} {method} {uri} ({ip}) {request_time:.2f}ms"
40 if status >= 300:
41 # log referers on redirects
42 ns['referer'] = request.headers.get('Referer', 'None')
43 msg = msg + ' referer={referer}'
44 if status >= 400:
45 # log user agent for failed requests
46 ns['agent'] = request.headers.get('User-Agent', 'Unknown')
47 msg = msg + ' user-agent={agent}'
48 if status >= 500 and status != 502:
49 # log all headers if it caused an error
50 log_method(json.dumps(request.headers, indent=2))
51 log_method(msg.format(**ns))
52
@@ -1,387 +1,387 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 Authors:
7 Authors:
8
8
9 * Evan Patterson
9 * Evan Patterson
10 * Min RK
10 * Min RK
11 * Erik Tollerud
11 * Erik Tollerud
12 * Fernando Perez
12 * Fernando Perez
13 * Bussonnier Matthias
13 * Bussonnier Matthias
14 * Thomas Kluyver
14 * Thomas Kluyver
15 * Paul Ivanov
15 * Paul Ivanov
16
16
17 """
17 """
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 # stdlib imports
23 # stdlib imports
24 import atexit
24 import atexit
25 import json
25 import json
26 import os
26 import os
27 import signal
27 import signal
28 import sys
28 import sys
29 import uuid
29 import uuid
30
30
31
31
32 # Local imports
32 # Local imports
33 from IPython.config.application import boolean_flag
33 from IPython.config.application import boolean_flag
34 from IPython.core.profiledir import ProfileDir
34 from IPython.core.profiledir import ProfileDir
35 from IPython.kernel.blocking import BlockingKernelClient
35 from IPython.kernel.blocking import BlockingKernelClient
36 from IPython.kernel import KernelManager
36 from IPython.kernel import KernelManager
37 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
37 from IPython.kernel import tunnel_to_kernel, find_connection_file, swallow_argv
38 from IPython.utils.path import filefind
38 from IPython.utils.path import filefind
39 from IPython.utils.py3compat import str_to_bytes
39 from IPython.utils.py3compat import str_to_bytes
40 from IPython.utils.traitlets import (
40 from IPython.utils.traitlets import (
41 Dict, List, Unicode, CUnicode, Int, CBool, Any
41 Dict, List, Unicode, CUnicode, Int, CBool, Any
42 )
42 )
43 from IPython.kernel.zmq.kernelapp import (
43 from IPython.kernel.zmq.kernelapp import (
44 kernel_flags,
44 kernel_flags,
45 kernel_aliases,
45 kernel_aliases,
46 IPKernelApp
46 IPKernelApp
47 )
47 )
48 from IPython.kernel.zmq.pylab.config import InlineBackend
48 from IPython.kernel.zmq.pylab.config import InlineBackend
49 from IPython.kernel.zmq.session import Session, default_secure
49 from IPython.kernel.zmq.session import Session, default_secure
50 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
51 from IPython.kernel.connect import ConnectionFileMixin
51 from IPython.kernel.connect import ConnectionFileMixin
52
52
53 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
54 # Network Constants
54 # Network Constants
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56
56
57 from IPython.utils.localinterfaces import localhost
57 from IPython.utils.localinterfaces import localhost
58
58
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60 # Globals
60 # Globals
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62
62
63
63
64 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
65 # Aliases and Flags
65 # Aliases and Flags
66 #-----------------------------------------------------------------------------
66 #-----------------------------------------------------------------------------
67
67
68 flags = dict(kernel_flags)
68 flags = dict(kernel_flags)
69
69
70 # the flags that are specific to the frontend
70 # the flags that are specific to the frontend
71 # these must be scrubbed before being passed to the kernel,
71 # these must be scrubbed before being passed to the kernel,
72 # or it will raise an error on unrecognized flags
72 # or it will raise an error on unrecognized flags
73 app_flags = {
73 app_flags = {
74 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
74 'existing' : ({'IPythonConsoleApp' : {'existing' : 'kernel*.json'}},
75 "Connect to an existing kernel. If no argument specified, guess most recent"),
75 "Connect to an existing kernel. If no argument specified, guess most recent"),
76 }
76 }
77 app_flags.update(boolean_flag(
77 app_flags.update(boolean_flag(
78 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
78 'confirm-exit', 'IPythonConsoleApp.confirm_exit',
79 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
79 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
80 to force a direct exit without any confirmation.
80 to force a direct exit without any confirmation.
81 """,
81 """,
82 """Don't prompt the user when exiting. This will terminate the kernel
82 """Don't prompt the user when exiting. This will terminate the kernel
83 if it is owned by the frontend, and leave it alive if it is external.
83 if it is owned by the frontend, and leave it alive if it is external.
84 """
84 """
85 ))
85 ))
86 flags.update(app_flags)
86 flags.update(app_flags)
87
87
88 aliases = dict(kernel_aliases)
88 aliases = dict(kernel_aliases)
89
89
90 # also scrub aliases from the frontend
90 # also scrub aliases from the frontend
91 app_aliases = dict(
91 app_aliases = dict(
92 ip = 'IPythonConsoleApp.ip',
92 ip = 'IPythonConsoleApp.ip',
93 transport = 'IPythonConsoleApp.transport',
93 transport = 'IPythonConsoleApp.transport',
94 hb = 'IPythonConsoleApp.hb_port',
94 hb = 'IPythonConsoleApp.hb_port',
95 shell = 'IPythonConsoleApp.shell_port',
95 shell = 'IPythonConsoleApp.shell_port',
96 iopub = 'IPythonConsoleApp.iopub_port',
96 iopub = 'IPythonConsoleApp.iopub_port',
97 stdin = 'IPythonConsoleApp.stdin_port',
97 stdin = 'IPythonConsoleApp.stdin_port',
98 existing = 'IPythonConsoleApp.existing',
98 existing = 'IPythonConsoleApp.existing',
99 f = 'IPythonConsoleApp.connection_file',
99 f = 'IPythonConsoleApp.connection_file',
100
100
101
101
102 ssh = 'IPythonConsoleApp.sshserver',
102 ssh = 'IPythonConsoleApp.sshserver',
103 )
103 )
104 aliases.update(app_aliases)
104 aliases.update(app_aliases)
105
105
106 #-----------------------------------------------------------------------------
106 #-----------------------------------------------------------------------------
107 # Classes
107 # Classes
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109
109
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111 # IPythonConsole
111 # IPythonConsole
112 #-----------------------------------------------------------------------------
112 #-----------------------------------------------------------------------------
113
113
114 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
114 classes = [IPKernelApp, ZMQInteractiveShell, KernelManager, ProfileDir, Session, InlineBackend]
115
115
116 class IPythonConsoleApp(ConnectionFileMixin):
116 class IPythonConsoleApp(ConnectionFileMixin):
117 name = 'ipython-console-mixin'
117 name = 'ipython-console-mixin'
118
118
119 description = """
119 description = """
120 The IPython Mixin Console.
120 The IPython Mixin Console.
121
121
122 This class contains the common portions of console client (QtConsole,
122 This class contains the common portions of console client (QtConsole,
123 ZMQ-based terminal console, etc). It is not a full console, in that
123 ZMQ-based terminal console, etc). It is not a full console, in that
124 launched terminal subprocesses will not be able to accept input.
124 launched terminal subprocesses will not be able to accept input.
125
125
126 The Console using this mixing supports various extra features beyond
126 The Console using this mixing supports various extra features beyond
127 the single-process Terminal IPython shell, such as connecting to
127 the single-process Terminal IPython shell, such as connecting to
128 existing kernel, via:
128 existing kernel, via:
129
129
130 ipython <appname> --existing
130 ipython <appname> --existing
131
131
132 as well as tunnel via SSH
132 as well as tunnel via SSH
133
133
134 """
134 """
135
135
136 classes = classes
136 classes = classes
137 flags = Dict(flags)
137 flags = Dict(flags)
138 aliases = Dict(aliases)
138 aliases = Dict(aliases)
139 kernel_manager_class = KernelManager
139 kernel_manager_class = KernelManager
140 kernel_client_class = BlockingKernelClient
140 kernel_client_class = BlockingKernelClient
141
141
142 kernel_argv = List(Unicode)
142 kernel_argv = List(Unicode)
143 # frontend flags&aliases to be stripped when building kernel_argv
143 # frontend flags&aliases to be stripped when building kernel_argv
144 frontend_flags = Any(app_flags)
144 frontend_flags = Any(app_flags)
145 frontend_aliases = Any(app_aliases)
145 frontend_aliases = Any(app_aliases)
146
146
147 # create requested profiles by default, if they don't exist:
147 # create requested profiles by default, if they don't exist:
148 auto_create = CBool(True)
148 auto_create = CBool(True)
149 # connection info:
149 # connection info:
150
150
151 sshserver = Unicode('', config=True,
151 sshserver = Unicode('', config=True,
152 help="""The SSH server to use to connect to the kernel.""")
152 help="""The SSH server to use to connect to the kernel.""")
153 sshkey = Unicode('', config=True,
153 sshkey = Unicode('', config=True,
154 help="""Path to the ssh key to use for logging in to the ssh server.""")
154 help="""Path to the ssh key to use for logging in to the ssh server.""")
155
155
156 hb_port = Int(0, config=True,
156 hb_port = Int(0, config=True,
157 help="set the heartbeat port [default: random]")
157 help="set the heartbeat port [default: random]")
158 shell_port = Int(0, config=True,
158 shell_port = Int(0, config=True,
159 help="set the shell (ROUTER) port [default: random]")
159 help="set the shell (ROUTER) port [default: random]")
160 iopub_port = Int(0, config=True,
160 iopub_port = Int(0, config=True,
161 help="set the iopub (PUB) port [default: random]")
161 help="set the iopub (PUB) port [default: random]")
162 stdin_port = Int(0, config=True,
162 stdin_port = Int(0, config=True,
163 help="set the stdin (DEALER) port [default: random]")
163 help="set the stdin (DEALER) port [default: random]")
164 connection_file = Unicode('', config=True,
164 connection_file = Unicode('', config=True,
165 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
165 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
166
166
167 This file will contain the IP, ports, and authentication key needed to connect
167 This file will contain the IP, ports, and authentication key needed to connect
168 clients to this kernel. By default, this file will be created in the security-dir
168 clients to this kernel. By default, this file will be created in the security-dir
169 of the current profile, but can be specified by absolute path.
169 of the current profile, but can be specified by absolute path.
170 """)
170 """)
171 def _connection_file_default(self):
171 def _connection_file_default(self):
172 return 'kernel-%i.json' % os.getpid()
172 return 'kernel-%i.json' % os.getpid()
173
173
174 existing = CUnicode('', config=True,
174 existing = CUnicode('', config=True,
175 help="""Connect to an already running kernel""")
175 help="""Connect to an already running kernel""")
176
176
177 confirm_exit = CBool(True, config=True,
177 confirm_exit = CBool(True, config=True,
178 help="""
178 help="""
179 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
179 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
180 to force a direct exit without any confirmation.""",
180 to force a direct exit without any confirmation.""",
181 )
181 )
182
182
183
183
184 def build_kernel_argv(self, argv=None):
184 def build_kernel_argv(self, argv=None):
185 """build argv to be passed to kernel subprocess"""
185 """build argv to be passed to kernel subprocess"""
186 if argv is None:
186 if argv is None:
187 argv = sys.argv[1:]
187 argv = sys.argv[1:]
188 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
188 self.kernel_argv = swallow_argv(argv, self.frontend_aliases, self.frontend_flags)
189 # kernel should inherit default config file from frontend
189 # kernel should inherit default config file from frontend
190 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
190 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
191
191
192 def init_connection_file(self):
192 def init_connection_file(self):
193 """find the connection file, and load the info if found.
193 """find the connection file, and load the info if found.
194
194
195 The current working directory and the current profile's security
195 The current working directory and the current profile's security
196 directory will be searched for the file if it is not given by
196 directory will be searched for the file if it is not given by
197 absolute path.
197 absolute path.
198
198
199 When attempting to connect to an existing kernel and the `--existing`
199 When attempting to connect to an existing kernel and the `--existing`
200 argument does not match an existing file, it will be interpreted as a
200 argument does not match an existing file, it will be interpreted as a
201 fileglob, and the matching file in the current profile's security dir
201 fileglob, and the matching file in the current profile's security dir
202 with the latest access time will be used.
202 with the latest access time will be used.
203
203
204 After this method is called, self.connection_file contains the *full path*
204 After this method is called, self.connection_file contains the *full path*
205 to the connection file, never just its name.
205 to the connection file, never just its name.
206 """
206 """
207 if self.existing:
207 if self.existing:
208 try:
208 try:
209 cf = find_connection_file(self.existing)
209 cf = find_connection_file(self.existing)
210 except Exception:
210 except Exception:
211 self.log.critical("Could not find existing kernel connection file %s", self.existing)
211 self.log.critical("Could not find existing kernel connection file %s", self.existing)
212 self.exit(1)
212 self.exit(1)
213 self.log.info("Connecting to existing kernel: %s" % cf)
213 self.log.debug("Connecting to existing kernel: %s" % cf)
214 self.connection_file = cf
214 self.connection_file = cf
215 else:
215 else:
216 # not existing, check if we are going to write the file
216 # not existing, check if we are going to write the file
217 # and ensure that self.connection_file is a full path, not just the shortname
217 # and ensure that self.connection_file is a full path, not just the shortname
218 try:
218 try:
219 cf = find_connection_file(self.connection_file)
219 cf = find_connection_file(self.connection_file)
220 except Exception:
220 except Exception:
221 # file might not exist
221 # file might not exist
222 if self.connection_file == os.path.basename(self.connection_file):
222 if self.connection_file == os.path.basename(self.connection_file):
223 # just shortname, put it in security dir
223 # just shortname, put it in security dir
224 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
224 cf = os.path.join(self.profile_dir.security_dir, self.connection_file)
225 else:
225 else:
226 cf = self.connection_file
226 cf = self.connection_file
227 self.connection_file = cf
227 self.connection_file = cf
228
228
229 # should load_connection_file only be used for existing?
229 # should load_connection_file only be used for existing?
230 # as it is now, this allows reusing ports if an existing
230 # as it is now, this allows reusing ports if an existing
231 # file is requested
231 # file is requested
232 try:
232 try:
233 self.load_connection_file()
233 self.load_connection_file()
234 except Exception:
234 except Exception:
235 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
235 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
236 self.exit(1)
236 self.exit(1)
237
237
238 def load_connection_file(self):
238 def load_connection_file(self):
239 """load ip/port/hmac config from JSON connection file"""
239 """load ip/port/hmac config from JSON connection file"""
240 # this is identical to IPKernelApp.load_connection_file
240 # this is identical to IPKernelApp.load_connection_file
241 # perhaps it can be centralized somewhere?
241 # perhaps it can be centralized somewhere?
242 try:
242 try:
243 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
243 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
244 except IOError:
244 except IOError:
245 self.log.debug("Connection File not found: %s", self.connection_file)
245 self.log.debug("Connection File not found: %s", self.connection_file)
246 return
246 return
247 self.log.debug(u"Loading connection file %s", fname)
247 self.log.debug(u"Loading connection file %s", fname)
248 with open(fname) as f:
248 with open(fname) as f:
249 cfg = json.load(f)
249 cfg = json.load(f)
250 self.transport = cfg.get('transport', 'tcp')
250 self.transport = cfg.get('transport', 'tcp')
251 self.ip = cfg.get('ip', localhost())
251 self.ip = cfg.get('ip', localhost())
252
252
253 for channel in ('hb', 'shell', 'iopub', 'stdin', 'control'):
253 for channel in ('hb', 'shell', 'iopub', 'stdin', 'control'):
254 name = channel + '_port'
254 name = channel + '_port'
255 if getattr(self, name) == 0 and name in cfg:
255 if getattr(self, name) == 0 and name in cfg:
256 # not overridden by config or cl_args
256 # not overridden by config or cl_args
257 setattr(self, name, cfg[name])
257 setattr(self, name, cfg[name])
258 if 'key' in cfg:
258 if 'key' in cfg:
259 self.config.Session.key = str_to_bytes(cfg['key'])
259 self.config.Session.key = str_to_bytes(cfg['key'])
260 if 'signature_scheme' in cfg:
260 if 'signature_scheme' in cfg:
261 self.config.Session.signature_scheme = cfg['signature_scheme']
261 self.config.Session.signature_scheme = cfg['signature_scheme']
262
262
263 def init_ssh(self):
263 def init_ssh(self):
264 """set up ssh tunnels, if needed."""
264 """set up ssh tunnels, if needed."""
265 if not self.existing or (not self.sshserver and not self.sshkey):
265 if not self.existing or (not self.sshserver and not self.sshkey):
266 return
266 return
267 self.load_connection_file()
267 self.load_connection_file()
268
268
269 transport = self.transport
269 transport = self.transport
270 ip = self.ip
270 ip = self.ip
271
271
272 if transport != 'tcp':
272 if transport != 'tcp':
273 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
273 self.log.error("Can only use ssh tunnels with TCP sockets, not %s", transport)
274 sys.exit(-1)
274 sys.exit(-1)
275
275
276 if self.sshkey and not self.sshserver:
276 if self.sshkey and not self.sshserver:
277 # specifying just the key implies that we are connecting directly
277 # specifying just the key implies that we are connecting directly
278 self.sshserver = ip
278 self.sshserver = ip
279 ip = localhost()
279 ip = localhost()
280
280
281 # build connection dict for tunnels:
281 # build connection dict for tunnels:
282 info = dict(ip=ip,
282 info = dict(ip=ip,
283 shell_port=self.shell_port,
283 shell_port=self.shell_port,
284 iopub_port=self.iopub_port,
284 iopub_port=self.iopub_port,
285 stdin_port=self.stdin_port,
285 stdin_port=self.stdin_port,
286 hb_port=self.hb_port
286 hb_port=self.hb_port
287 )
287 )
288
288
289 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
289 self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver))
290
290
291 # tunnels return a new set of ports, which will be on localhost:
291 # tunnels return a new set of ports, which will be on localhost:
292 self.ip = localhost()
292 self.ip = localhost()
293 try:
293 try:
294 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
294 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
295 except:
295 except:
296 # even catch KeyboardInterrupt
296 # even catch KeyboardInterrupt
297 self.log.error("Could not setup tunnels", exc_info=True)
297 self.log.error("Could not setup tunnels", exc_info=True)
298 self.exit(1)
298 self.exit(1)
299
299
300 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
300 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
301
301
302 cf = self.connection_file
302 cf = self.connection_file
303 base,ext = os.path.splitext(cf)
303 base,ext = os.path.splitext(cf)
304 base = os.path.basename(base)
304 base = os.path.basename(base)
305 self.connection_file = os.path.basename(base)+'-ssh'+ext
305 self.connection_file = os.path.basename(base)+'-ssh'+ext
306 self.log.critical("To connect another client via this tunnel, use:")
306 self.log.info("To connect another client via this tunnel, use:")
307 self.log.critical("--existing %s" % self.connection_file)
307 self.log.info("--existing %s" % self.connection_file)
308
308
309 def _new_connection_file(self):
309 def _new_connection_file(self):
310 cf = ''
310 cf = ''
311 while not cf:
311 while not cf:
312 # we don't need a 128b id to distinguish kernels, use more readable
312 # we don't need a 128b id to distinguish kernels, use more readable
313 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
313 # 48b node segment (12 hex chars). Users running more than 32k simultaneous
314 # kernels can subclass.
314 # kernels can subclass.
315 ident = str(uuid.uuid4()).split('-')[-1]
315 ident = str(uuid.uuid4()).split('-')[-1]
316 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
316 cf = os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % ident)
317 # only keep if it's actually new. Protect against unlikely collision
317 # only keep if it's actually new. Protect against unlikely collision
318 # in 48b random search space
318 # in 48b random search space
319 cf = cf if not os.path.exists(cf) else ''
319 cf = cf if not os.path.exists(cf) else ''
320 return cf
320 return cf
321
321
322 def init_kernel_manager(self):
322 def init_kernel_manager(self):
323 # Don't let Qt or ZMQ swallow KeyboardInterupts.
323 # Don't let Qt or ZMQ swallow KeyboardInterupts.
324 if self.existing:
324 if self.existing:
325 self.kernel_manager = None
325 self.kernel_manager = None
326 return
326 return
327 signal.signal(signal.SIGINT, signal.SIG_DFL)
327 signal.signal(signal.SIGINT, signal.SIG_DFL)
328
328
329 # Create a KernelManager and start a kernel.
329 # Create a KernelManager and start a kernel.
330 self.kernel_manager = self.kernel_manager_class(
330 self.kernel_manager = self.kernel_manager_class(
331 ip=self.ip,
331 ip=self.ip,
332 transport=self.transport,
332 transport=self.transport,
333 shell_port=self.shell_port,
333 shell_port=self.shell_port,
334 iopub_port=self.iopub_port,
334 iopub_port=self.iopub_port,
335 stdin_port=self.stdin_port,
335 stdin_port=self.stdin_port,
336 hb_port=self.hb_port,
336 hb_port=self.hb_port,
337 connection_file=self.connection_file,
337 connection_file=self.connection_file,
338 parent=self,
338 parent=self,
339 )
339 )
340 self.kernel_manager.client_factory = self.kernel_client_class
340 self.kernel_manager.client_factory = self.kernel_client_class
341 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
341 self.kernel_manager.start_kernel(extra_arguments=self.kernel_argv)
342 atexit.register(self.kernel_manager.cleanup_ipc_files)
342 atexit.register(self.kernel_manager.cleanup_ipc_files)
343
343
344 if self.sshserver:
344 if self.sshserver:
345 # ssh, write new connection file
345 # ssh, write new connection file
346 self.kernel_manager.write_connection_file()
346 self.kernel_manager.write_connection_file()
347
347
348 # in case KM defaults / ssh writing changes things:
348 # in case KM defaults / ssh writing changes things:
349 km = self.kernel_manager
349 km = self.kernel_manager
350 self.shell_port=km.shell_port
350 self.shell_port=km.shell_port
351 self.iopub_port=km.iopub_port
351 self.iopub_port=km.iopub_port
352 self.stdin_port=km.stdin_port
352 self.stdin_port=km.stdin_port
353 self.hb_port=km.hb_port
353 self.hb_port=km.hb_port
354 self.connection_file = km.connection_file
354 self.connection_file = km.connection_file
355
355
356 atexit.register(self.kernel_manager.cleanup_connection_file)
356 atexit.register(self.kernel_manager.cleanup_connection_file)
357
357
358 def init_kernel_client(self):
358 def init_kernel_client(self):
359 if self.kernel_manager is not None:
359 if self.kernel_manager is not None:
360 self.kernel_client = self.kernel_manager.client()
360 self.kernel_client = self.kernel_manager.client()
361 else:
361 else:
362 self.kernel_client = self.kernel_client_class(
362 self.kernel_client = self.kernel_client_class(
363 ip=self.ip,
363 ip=self.ip,
364 transport=self.transport,
364 transport=self.transport,
365 shell_port=self.shell_port,
365 shell_port=self.shell_port,
366 iopub_port=self.iopub_port,
366 iopub_port=self.iopub_port,
367 stdin_port=self.stdin_port,
367 stdin_port=self.stdin_port,
368 hb_port=self.hb_port,
368 hb_port=self.hb_port,
369 connection_file=self.connection_file,
369 connection_file=self.connection_file,
370 parent=self,
370 parent=self,
371 )
371 )
372
372
373 self.kernel_client.start_channels()
373 self.kernel_client.start_channels()
374
374
375
375
376
376
377 def initialize(self, argv=None):
377 def initialize(self, argv=None):
378 """
378 """
379 Classes which mix this class in should call:
379 Classes which mix this class in should call:
380 IPythonConsoleApp.initialize(self,argv)
380 IPythonConsoleApp.initialize(self,argv)
381 """
381 """
382 self.init_connection_file()
382 self.init_connection_file()
383 default_secure(self.config)
383 default_secure(self.config)
384 self.init_ssh()
384 self.init_ssh()
385 self.init_kernel_manager()
385 self.init_kernel_manager()
386 self.init_kernel_client()
386 self.init_kernel_client()
387
387
@@ -1,836 +1,837 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 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 from __future__ import print_function
8 from __future__ import print_function
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2013 The IPython Development Team
10 # Copyright (C) 2013 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # stdlib
20 # stdlib
21 import errno
21 import errno
22 import io
22 import io
23 import json
23 import json
24 import logging
24 import logging
25 import os
25 import os
26 import random
26 import random
27 import select
27 import select
28 import signal
28 import signal
29 import socket
29 import socket
30 import sys
30 import sys
31 import threading
31 import threading
32 import time
32 import time
33 import webbrowser
33 import webbrowser
34
34
35
35
36 # Third party
36 # Third party
37 # check for pyzmq 2.1.11
37 # check for pyzmq 2.1.11
38 from IPython.utils.zmqrelated import check_for_zmq
38 from IPython.utils.zmqrelated import check_for_zmq
39 check_for_zmq('2.1.11', 'IPython.html')
39 check_for_zmq('2.1.11', 'IPython.html')
40
40
41 from jinja2 import Environment, FileSystemLoader
41 from jinja2 import Environment, FileSystemLoader
42
42
43 # Install the pyzmq ioloop. This has to be done before anything else from
43 # Install the pyzmq ioloop. This has to be done before anything else from
44 # tornado is imported.
44 # tornado is imported.
45 from zmq.eventloop import ioloop
45 from zmq.eventloop import ioloop
46 ioloop.install()
46 ioloop.install()
47
47
48 # check for tornado 3.1.0
48 # check for tornado 3.1.0
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 try:
50 try:
51 import tornado
51 import tornado
52 except ImportError:
52 except ImportError:
53 raise ImportError(msg)
53 raise ImportError(msg)
54 try:
54 try:
55 version_info = tornado.version_info
55 version_info = tornado.version_info
56 except AttributeError:
56 except AttributeError:
57 raise ImportError(msg + ", but you have < 1.1.0")
57 raise ImportError(msg + ", but you have < 1.1.0")
58 if version_info < (3,1,0):
58 if version_info < (3,1,0):
59 raise ImportError(msg + ", but you have %s" % tornado.version)
59 raise ImportError(msg + ", but you have %s" % tornado.version)
60
60
61 from tornado import httpserver
61 from tornado import httpserver
62 from tornado import web
62 from tornado import web
63
63
64 # Our own libraries
64 # Our own libraries
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 from .base.handlers import Template404
66 from .base.handlers import Template404
67
67 from .log import log_request
68 from .services.kernels.kernelmanager import MappingKernelManager
68 from .services.kernels.kernelmanager import MappingKernelManager
69 from .services.notebooks.nbmanager import NotebookManager
69 from .services.notebooks.nbmanager import NotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
70 from .services.notebooks.filenbmanager import FileNotebookManager
71 from .services.clusters.clustermanager import ClusterManager
71 from .services.clusters.clustermanager import ClusterManager
72 from .services.sessions.sessionmanager import SessionManager
72 from .services.sessions.sessionmanager import SessionManager
73
73
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75
75
76 from IPython.config.application import catch_config_error, boolean_flag
76 from IPython.config.application import catch_config_error, boolean_flag
77 from IPython.core.application import BaseIPythonApplication
77 from IPython.core.application import BaseIPythonApplication
78 from IPython.core.profiledir import ProfileDir
78 from IPython.core.profiledir import ProfileDir
79 from IPython.consoleapp import IPythonConsoleApp
79 from IPython.consoleapp import IPythonConsoleApp
80 from IPython.kernel import swallow_argv
80 from IPython.kernel import swallow_argv
81 from IPython.kernel.zmq.session import default_secure
81 from IPython.kernel.zmq.session import default_secure
82 from IPython.kernel.zmq.kernelapp import (
82 from IPython.kernel.zmq.kernelapp import (
83 kernel_flags,
83 kernel_flags,
84 kernel_aliases,
84 kernel_aliases,
85 )
85 )
86 from IPython.utils.importstring import import_item
86 from IPython.utils.importstring import import_item
87 from IPython.utils.localinterfaces import localhost
87 from IPython.utils.localinterfaces import localhost
88 from IPython.utils import submodule
88 from IPython.utils import submodule
89 from IPython.utils.traitlets import (
89 from IPython.utils.traitlets import (
90 Dict, Unicode, Integer, List, Bool, Bytes,
90 Dict, Unicode, Integer, List, Bool, Bytes,
91 DottedObjectName
91 DottedObjectName
92 )
92 )
93 from IPython.utils import py3compat
93 from IPython.utils import py3compat
94 from IPython.utils.path import filefind, get_ipython_dir
94 from IPython.utils.path import filefind, get_ipython_dir
95
95
96 from .utils import url_path_join
96 from .utils import url_path_join
97
97
98 #-----------------------------------------------------------------------------
98 #-----------------------------------------------------------------------------
99 # Module globals
99 # Module globals
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101
101
102 _examples = """
102 _examples = """
103 ipython notebook # start the notebook
103 ipython notebook # start the notebook
104 ipython notebook --profile=sympy # use the sympy profile
104 ipython notebook --profile=sympy # use the sympy profile
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
105 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 """
106 """
107
107
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109 # Helper functions
109 # Helper functions
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111
111
112 def random_ports(port, n):
112 def random_ports(port, n):
113 """Generate a list of n random ports near the given port.
113 """Generate a list of n random ports near the given port.
114
114
115 The first 5 ports will be sequential, and the remaining n-5 will be
115 The first 5 ports will be sequential, and the remaining n-5 will be
116 randomly selected in the range [port-2*n, port+2*n].
116 randomly selected in the range [port-2*n, port+2*n].
117 """
117 """
118 for i in range(min(5, n)):
118 for i in range(min(5, n)):
119 yield port + i
119 yield port + i
120 for i in range(n-5):
120 for i in range(n-5):
121 yield max(1, port + random.randint(-2*n, 2*n))
121 yield max(1, port + random.randint(-2*n, 2*n))
122
122
123 def load_handlers(name):
123 def load_handlers(name):
124 """Load the (URL pattern, handler) tuples for each component."""
124 """Load the (URL pattern, handler) tuples for each component."""
125 name = 'IPython.html.' + name
125 name = 'IPython.html.' + name
126 mod = __import__(name, fromlist=['default_handlers'])
126 mod = __import__(name, fromlist=['default_handlers'])
127 return mod.default_handlers
127 return mod.default_handlers
128
128
129 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
130 # The Tornado web application
130 # The Tornado web application
131 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
132
132
133 class NotebookWebApplication(web.Application):
133 class NotebookWebApplication(web.Application):
134
134
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
135 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 cluster_manager, session_manager, log, base_project_url,
136 cluster_manager, session_manager, log, base_project_url,
137 settings_overrides):
137 settings_overrides):
138
138
139 settings = self.init_settings(
139 settings = self.init_settings(
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
140 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 session_manager, log, base_project_url, settings_overrides)
141 session_manager, log, base_project_url, settings_overrides)
142 handlers = self.init_handlers(settings)
142 handlers = self.init_handlers(settings)
143
143
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145
145
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
146 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 cluster_manager, session_manager, log, base_project_url,
147 cluster_manager, session_manager, log, base_project_url,
148 settings_overrides):
148 settings_overrides):
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
149 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 # base_project_url will always be unicode, which will in turn
150 # base_project_url will always be unicode, which will in turn
151 # make the patterns unicode, and ultimately result in unicode
151 # make the patterns unicode, and ultimately result in unicode
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
152 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 # This enforces that base_project_url be ascii in that situation.
153 # This enforces that base_project_url be ascii in that situation.
154 #
154 #
155 # Note that the URLs these patterns check against are escaped,
155 # Note that the URLs these patterns check against are escaped,
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
156 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
157 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
158 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 settings = dict(
159 settings = dict(
160 # basics
160 # basics
161 log_function=log_request,
161 base_project_url=base_project_url,
162 base_project_url=base_project_url,
162 base_kernel_url=ipython_app.base_kernel_url,
163 base_kernel_url=ipython_app.base_kernel_url,
163 template_path=template_path,
164 template_path=template_path,
164 static_path=ipython_app.static_file_path,
165 static_path=ipython_app.static_file_path,
165 static_handler_class = FileFindHandler,
166 static_handler_class = FileFindHandler,
166 static_url_prefix = url_path_join(base_project_url,'/static/'),
167 static_url_prefix = url_path_join(base_project_url,'/static/'),
167
168
168 # authentication
169 # authentication
169 cookie_secret=ipython_app.cookie_secret,
170 cookie_secret=ipython_app.cookie_secret,
170 login_url=url_path_join(base_project_url,'/login'),
171 login_url=url_path_join(base_project_url,'/login'),
171 password=ipython_app.password,
172 password=ipython_app.password,
172
173
173 # managers
174 # managers
174 kernel_manager=kernel_manager,
175 kernel_manager=kernel_manager,
175 notebook_manager=notebook_manager,
176 notebook_manager=notebook_manager,
176 cluster_manager=cluster_manager,
177 cluster_manager=cluster_manager,
177 session_manager=session_manager,
178 session_manager=session_manager,
178
179
179 # IPython stuff
180 # IPython stuff
180 nbextensions_path = ipython_app.nbextensions_path,
181 nbextensions_path = ipython_app.nbextensions_path,
181 mathjax_url=ipython_app.mathjax_url,
182 mathjax_url=ipython_app.mathjax_url,
182 config=ipython_app.config,
183 config=ipython_app.config,
183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
184 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
184 )
185 )
185
186
186 # allow custom overrides for the tornado web app.
187 # allow custom overrides for the tornado web app.
187 settings.update(settings_overrides)
188 settings.update(settings_overrides)
188 return settings
189 return settings
189
190
190 def init_handlers(self, settings):
191 def init_handlers(self, settings):
191 # Load the (URL pattern, handler) tuples for each component.
192 # Load the (URL pattern, handler) tuples for each component.
192 handlers = []
193 handlers = []
193 handlers.extend(load_handlers('base.handlers'))
194 handlers.extend(load_handlers('base.handlers'))
194 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('tree.handlers'))
195 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.login'))
196 handlers.extend(load_handlers('auth.logout'))
197 handlers.extend(load_handlers('auth.logout'))
197 handlers.extend(load_handlers('notebook.handlers'))
198 handlers.extend(load_handlers('notebook.handlers'))
198 handlers.extend(load_handlers('nbconvert.handlers'))
199 handlers.extend(load_handlers('nbconvert.handlers'))
199 handlers.extend(load_handlers('services.kernels.handlers'))
200 handlers.extend(load_handlers('services.kernels.handlers'))
200 handlers.extend(load_handlers('services.notebooks.handlers'))
201 handlers.extend(load_handlers('services.notebooks.handlers'))
201 handlers.extend(load_handlers('services.clusters.handlers'))
202 handlers.extend(load_handlers('services.clusters.handlers'))
202 handlers.extend(load_handlers('services.sessions.handlers'))
203 handlers.extend(load_handlers('services.sessions.handlers'))
203 handlers.extend(load_handlers('services.nbconvert.handlers'))
204 handlers.extend(load_handlers('services.nbconvert.handlers'))
204 handlers.extend([
205 handlers.extend([
205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
206 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
207 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
207 ])
208 ])
208 # prepend base_project_url onto the patterns that we match
209 # prepend base_project_url onto the patterns that we match
209 new_handlers = []
210 new_handlers = []
210 for handler in handlers:
211 for handler in handlers:
211 pattern = url_path_join(settings['base_project_url'], handler[0])
212 pattern = url_path_join(settings['base_project_url'], handler[0])
212 new_handler = tuple([pattern] + list(handler[1:]))
213 new_handler = tuple([pattern] + list(handler[1:]))
213 new_handlers.append(new_handler)
214 new_handlers.append(new_handler)
214 # add 404 on the end, which will catch everything that falls through
215 # add 404 on the end, which will catch everything that falls through
215 new_handlers.append((r'(.*)', Template404))
216 new_handlers.append((r'(.*)', Template404))
216 return new_handlers
217 return new_handlers
217
218
218
219
219 class NbserverListApp(BaseIPythonApplication):
220 class NbserverListApp(BaseIPythonApplication):
220
221
221 description="List currently running notebook servers in this profile."
222 description="List currently running notebook servers in this profile."
222
223
223 flags = dict(
224 flags = dict(
224 json=({'NbserverListApp': {'json': True}},
225 json=({'NbserverListApp': {'json': True}},
225 "Produce machine-readable JSON output."),
226 "Produce machine-readable JSON output."),
226 )
227 )
227
228
228 json = Bool(False, config=True,
229 json = Bool(False, config=True,
229 help="If True, each line of output will be a JSON object with the "
230 help="If True, each line of output will be a JSON object with the "
230 "details from the server info file.")
231 "details from the server info file.")
231
232
232 def start(self):
233 def start(self):
233 if not self.json:
234 if not self.json:
234 print("Currently running servers:")
235 print("Currently running servers:")
235 for serverinfo in list_running_servers(self.profile):
236 for serverinfo in list_running_servers(self.profile):
236 if self.json:
237 if self.json:
237 print(json.dumps(serverinfo))
238 print(json.dumps(serverinfo))
238 else:
239 else:
239 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
240
241
241 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
242 # Aliases and Flags
243 # Aliases and Flags
243 #-----------------------------------------------------------------------------
244 #-----------------------------------------------------------------------------
244
245
245 flags = dict(kernel_flags)
246 flags = dict(kernel_flags)
246 flags['no-browser']=(
247 flags['no-browser']=(
247 {'NotebookApp' : {'open_browser' : False}},
248 {'NotebookApp' : {'open_browser' : False}},
248 "Don't open the notebook in a browser after startup."
249 "Don't open the notebook in a browser after startup."
249 )
250 )
250 flags['no-mathjax']=(
251 flags['no-mathjax']=(
251 {'NotebookApp' : {'enable_mathjax' : False}},
252 {'NotebookApp' : {'enable_mathjax' : False}},
252 """Disable MathJax
253 """Disable MathJax
253
254
254 MathJax is the javascript library IPython uses to render math/LaTeX. It is
255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
255 very large, so you may want to disable it if you have a slow internet
256 very large, so you may want to disable it if you have a slow internet
256 connection, or for offline use of the notebook.
257 connection, or for offline use of the notebook.
257
258
258 When disabled, equations etc. will appear as their untransformed TeX source.
259 When disabled, equations etc. will appear as their untransformed TeX source.
259 """
260 """
260 )
261 )
261
262
262 # Add notebook manager flags
263 # Add notebook manager flags
263 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
264 'Auto-save a .py script everytime the .ipynb notebook is saved',
265 'Auto-save a .py script everytime the .ipynb notebook is saved',
265 'Do not auto-save .py scripts for every notebook'))
266 'Do not auto-save .py scripts for every notebook'))
266
267
267 # the flags that are specific to the frontend
268 # the flags that are specific to the frontend
268 # these must be scrubbed before being passed to the kernel,
269 # these must be scrubbed before being passed to the kernel,
269 # or it will raise an error on unrecognized flags
270 # or it will raise an error on unrecognized flags
270 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
271 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
271
272
272 aliases = dict(kernel_aliases)
273 aliases = dict(kernel_aliases)
273
274
274 aliases.update({
275 aliases.update({
275 'ip': 'NotebookApp.ip',
276 'ip': 'NotebookApp.ip',
276 'port': 'NotebookApp.port',
277 'port': 'NotebookApp.port',
277 'port-retries': 'NotebookApp.port_retries',
278 'port-retries': 'NotebookApp.port_retries',
278 'transport': 'KernelManager.transport',
279 'transport': 'KernelManager.transport',
279 'keyfile': 'NotebookApp.keyfile',
280 'keyfile': 'NotebookApp.keyfile',
280 'certfile': 'NotebookApp.certfile',
281 'certfile': 'NotebookApp.certfile',
281 'notebook-dir': 'NotebookManager.notebook_dir',
282 'notebook-dir': 'NotebookManager.notebook_dir',
282 'browser': 'NotebookApp.browser',
283 'browser': 'NotebookApp.browser',
283 })
284 })
284
285
285 # remove ipkernel flags that are singletons, and don't make sense in
286 # remove ipkernel flags that are singletons, and don't make sense in
286 # multi-kernel evironment:
287 # multi-kernel evironment:
287 aliases.pop('f', None)
288 aliases.pop('f', None)
288
289
289 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
290 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
290 u'notebook-dir', u'profile', u'profile-dir']
291 u'notebook-dir', u'profile', u'profile-dir']
291
292
292 #-----------------------------------------------------------------------------
293 #-----------------------------------------------------------------------------
293 # NotebookApp
294 # NotebookApp
294 #-----------------------------------------------------------------------------
295 #-----------------------------------------------------------------------------
295
296
296 class NotebookApp(BaseIPythonApplication):
297 class NotebookApp(BaseIPythonApplication):
297
298
298 name = 'ipython-notebook'
299 name = 'ipython-notebook'
299
300
300 description = """
301 description = """
301 The IPython HTML Notebook.
302 The IPython HTML Notebook.
302
303
303 This launches a Tornado based HTML Notebook Server that serves up an
304 This launches a Tornado based HTML Notebook Server that serves up an
304 HTML5/Javascript Notebook client.
305 HTML5/Javascript Notebook client.
305 """
306 """
306 examples = _examples
307 examples = _examples
307
308
308 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
309 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
309 FileNotebookManager]
310 FileNotebookManager]
310 flags = Dict(flags)
311 flags = Dict(flags)
311 aliases = Dict(aliases)
312 aliases = Dict(aliases)
312
313
313 subcommands = dict(
314 subcommands = dict(
314 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
315 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
315 )
316 )
316
317
317 kernel_argv = List(Unicode)
318 kernel_argv = List(Unicode)
318
319
319 def _log_level_default(self):
320 def _log_level_default(self):
320 return logging.INFO
321 return logging.INFO
321
322
322 def _log_format_default(self):
323 def _log_format_default(self):
323 """override default log format to include time"""
324 """override default log format to include time"""
324 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
325 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
325
326
326 # create requested profiles by default, if they don't exist:
327 # create requested profiles by default, if they don't exist:
327 auto_create = Bool(True)
328 auto_create = Bool(True)
328
329
329 # file to be opened in the notebook server
330 # file to be opened in the notebook server
330 file_to_run = Unicode('')
331 file_to_run = Unicode('')
331
332
332 # Network related information.
333 # Network related information.
333
334
334 ip = Unicode(config=True,
335 ip = Unicode(config=True,
335 help="The IP address the notebook server will listen on."
336 help="The IP address the notebook server will listen on."
336 )
337 )
337 def _ip_default(self):
338 def _ip_default(self):
338 return localhost()
339 return localhost()
339
340
340 def _ip_changed(self, name, old, new):
341 def _ip_changed(self, name, old, new):
341 if new == u'*': self.ip = u''
342 if new == u'*': self.ip = u''
342
343
343 port = Integer(8888, config=True,
344 port = Integer(8888, config=True,
344 help="The port the notebook server will listen on."
345 help="The port the notebook server will listen on."
345 )
346 )
346 port_retries = Integer(50, config=True,
347 port_retries = Integer(50, config=True,
347 help="The number of additional ports to try if the specified port is not available."
348 help="The number of additional ports to try if the specified port is not available."
348 )
349 )
349
350
350 certfile = Unicode(u'', config=True,
351 certfile = Unicode(u'', config=True,
351 help="""The full path to an SSL/TLS certificate file."""
352 help="""The full path to an SSL/TLS certificate file."""
352 )
353 )
353
354
354 keyfile = Unicode(u'', config=True,
355 keyfile = Unicode(u'', config=True,
355 help="""The full path to a private key file for usage with SSL/TLS."""
356 help="""The full path to a private key file for usage with SSL/TLS."""
356 )
357 )
357
358
358 cookie_secret = Bytes(b'', config=True,
359 cookie_secret = Bytes(b'', config=True,
359 help="""The random bytes used to secure cookies.
360 help="""The random bytes used to secure cookies.
360 By default this is a new random number every time you start the Notebook.
361 By default this is a new random number every time you start the Notebook.
361 Set it to a value in a config file to enable logins to persist across server sessions.
362 Set it to a value in a config file to enable logins to persist across server sessions.
362
363
363 Note: Cookie secrets should be kept private, do not share config files with
364 Note: Cookie secrets should be kept private, do not share config files with
364 cookie_secret stored in plaintext (you can read the value from a file).
365 cookie_secret stored in plaintext (you can read the value from a file).
365 """
366 """
366 )
367 )
367 def _cookie_secret_default(self):
368 def _cookie_secret_default(self):
368 return os.urandom(1024)
369 return os.urandom(1024)
369
370
370 password = Unicode(u'', config=True,
371 password = Unicode(u'', config=True,
371 help="""Hashed password to use for web authentication.
372 help="""Hashed password to use for web authentication.
372
373
373 To generate, type in a python/IPython shell:
374 To generate, type in a python/IPython shell:
374
375
375 from IPython.lib import passwd; passwd()
376 from IPython.lib import passwd; passwd()
376
377
377 The string should be of the form type:salt:hashed-password.
378 The string should be of the form type:salt:hashed-password.
378 """
379 """
379 )
380 )
380
381
381 open_browser = Bool(True, config=True,
382 open_browser = Bool(True, config=True,
382 help="""Whether to open in a browser after starting.
383 help="""Whether to open in a browser after starting.
383 The specific browser used is platform dependent and
384 The specific browser used is platform dependent and
384 determined by the python standard library `webbrowser`
385 determined by the python standard library `webbrowser`
385 module, unless it is overridden using the --browser
386 module, unless it is overridden using the --browser
386 (NotebookApp.browser) configuration option.
387 (NotebookApp.browser) configuration option.
387 """)
388 """)
388
389
389 browser = Unicode(u'', config=True,
390 browser = Unicode(u'', config=True,
390 help="""Specify what command to use to invoke a web
391 help="""Specify what command to use to invoke a web
391 browser when opening the notebook. If not specified, the
392 browser when opening the notebook. If not specified, the
392 default browser will be determined by the `webbrowser`
393 default browser will be determined by the `webbrowser`
393 standard library module, which allows setting of the
394 standard library module, which allows setting of the
394 BROWSER environment variable to override it.
395 BROWSER environment variable to override it.
395 """)
396 """)
396
397
397 webapp_settings = Dict(config=True,
398 webapp_settings = Dict(config=True,
398 help="Supply overrides for the tornado.web.Application that the "
399 help="Supply overrides for the tornado.web.Application that the "
399 "IPython notebook uses.")
400 "IPython notebook uses.")
400
401
401 enable_mathjax = Bool(True, config=True,
402 enable_mathjax = Bool(True, config=True,
402 help="""Whether to enable MathJax for typesetting math/TeX
403 help="""Whether to enable MathJax for typesetting math/TeX
403
404
404 MathJax is the javascript library IPython uses to render math/LaTeX. It is
405 MathJax is the javascript library IPython uses to render math/LaTeX. It is
405 very large, so you may want to disable it if you have a slow internet
406 very large, so you may want to disable it if you have a slow internet
406 connection, or for offline use of the notebook.
407 connection, or for offline use of the notebook.
407
408
408 When disabled, equations etc. will appear as their untransformed TeX source.
409 When disabled, equations etc. will appear as their untransformed TeX source.
409 """
410 """
410 )
411 )
411 def _enable_mathjax_changed(self, name, old, new):
412 def _enable_mathjax_changed(self, name, old, new):
412 """set mathjax url to empty if mathjax is disabled"""
413 """set mathjax url to empty if mathjax is disabled"""
413 if not new:
414 if not new:
414 self.mathjax_url = u''
415 self.mathjax_url = u''
415
416
416 base_project_url = Unicode('/', config=True,
417 base_project_url = Unicode('/', config=True,
417 help='''The base URL for the notebook server.
418 help='''The base URL for the notebook server.
418
419
419 Leading and trailing slashes can be omitted,
420 Leading and trailing slashes can be omitted,
420 and will automatically be added.
421 and will automatically be added.
421 ''')
422 ''')
422 def _base_project_url_changed(self, name, old, new):
423 def _base_project_url_changed(self, name, old, new):
423 if not new.startswith('/'):
424 if not new.startswith('/'):
424 self.base_project_url = '/'+new
425 self.base_project_url = '/'+new
425 elif not new.endswith('/'):
426 elif not new.endswith('/'):
426 self.base_project_url = new+'/'
427 self.base_project_url = new+'/'
427
428
428 base_kernel_url = Unicode('/', config=True,
429 base_kernel_url = Unicode('/', config=True,
429 help='''The base URL for the kernel server
430 help='''The base URL for the kernel server
430
431
431 Leading and trailing slashes can be omitted,
432 Leading and trailing slashes can be omitted,
432 and will automatically be added.
433 and will automatically be added.
433 ''')
434 ''')
434 def _base_kernel_url_changed(self, name, old, new):
435 def _base_kernel_url_changed(self, name, old, new):
435 if not new.startswith('/'):
436 if not new.startswith('/'):
436 self.base_kernel_url = '/'+new
437 self.base_kernel_url = '/'+new
437 elif not new.endswith('/'):
438 elif not new.endswith('/'):
438 self.base_kernel_url = new+'/'
439 self.base_kernel_url = new+'/'
439
440
440 websocket_url = Unicode("", config=True,
441 websocket_url = Unicode("", config=True,
441 help="""The base URL for the websocket server,
442 help="""The base URL for the websocket server,
442 if it differs from the HTTP server (hint: it almost certainly doesn't).
443 if it differs from the HTTP server (hint: it almost certainly doesn't).
443
444
444 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
445 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
445 """
446 """
446 )
447 )
447
448
448 extra_static_paths = List(Unicode, config=True,
449 extra_static_paths = List(Unicode, config=True,
449 help="""Extra paths to search for serving static files.
450 help="""Extra paths to search for serving static files.
450
451
451 This allows adding javascript/css to be available from the notebook server machine,
452 This allows adding javascript/css to be available from the notebook server machine,
452 or overriding individual files in the IPython"""
453 or overriding individual files in the IPython"""
453 )
454 )
454 def _extra_static_paths_default(self):
455 def _extra_static_paths_default(self):
455 return [os.path.join(self.profile_dir.location, 'static')]
456 return [os.path.join(self.profile_dir.location, 'static')]
456
457
457 @property
458 @property
458 def static_file_path(self):
459 def static_file_path(self):
459 """return extra paths + the default location"""
460 """return extra paths + the default location"""
460 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
461 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
461
462
462 nbextensions_path = List(Unicode, config=True,
463 nbextensions_path = List(Unicode, config=True,
463 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
464 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
464 )
465 )
465 def _nbextensions_path_default(self):
466 def _nbextensions_path_default(self):
466 return [os.path.join(get_ipython_dir(), 'nbextensions')]
467 return [os.path.join(get_ipython_dir(), 'nbextensions')]
467
468
468 mathjax_url = Unicode("", config=True,
469 mathjax_url = Unicode("", config=True,
469 help="""The url for MathJax.js."""
470 help="""The url for MathJax.js."""
470 )
471 )
471 def _mathjax_url_default(self):
472 def _mathjax_url_default(self):
472 if not self.enable_mathjax:
473 if not self.enable_mathjax:
473 return u''
474 return u''
474 static_url_prefix = self.webapp_settings.get("static_url_prefix",
475 static_url_prefix = self.webapp_settings.get("static_url_prefix",
475 url_path_join(self.base_project_url, "static")
476 url_path_join(self.base_project_url, "static")
476 )
477 )
477
478
478 # try local mathjax, either in nbextensions/mathjax or static/mathjax
479 # try local mathjax, either in nbextensions/mathjax or static/mathjax
479 for (url_prefix, search_path) in [
480 for (url_prefix, search_path) in [
480 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
481 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
481 (static_url_prefix, self.static_file_path),
482 (static_url_prefix, self.static_file_path),
482 ]:
483 ]:
483 self.log.debug("searching for local mathjax in %s", search_path)
484 self.log.debug("searching for local mathjax in %s", search_path)
484 try:
485 try:
485 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
486 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
486 except IOError:
487 except IOError:
487 continue
488 continue
488 else:
489 else:
489 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
490 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
490 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
491 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
491 return url
492 return url
492
493
493 # no local mathjax, serve from CDN
494 # no local mathjax, serve from CDN
494 if self.certfile:
495 if self.certfile:
495 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
496 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
496 host = u"https://c328740.ssl.cf1.rackcdn.com"
497 host = u"https://c328740.ssl.cf1.rackcdn.com"
497 else:
498 else:
498 host = u"http://cdn.mathjax.org"
499 host = u"http://cdn.mathjax.org"
499
500
500 url = host + u"/mathjax/latest/MathJax.js"
501 url = host + u"/mathjax/latest/MathJax.js"
501 self.log.info("Using MathJax from CDN: %s", url)
502 self.log.info("Using MathJax from CDN: %s", url)
502 return url
503 return url
503
504
504 def _mathjax_url_changed(self, name, old, new):
505 def _mathjax_url_changed(self, name, old, new):
505 if new and not self.enable_mathjax:
506 if new and not self.enable_mathjax:
506 # enable_mathjax=False overrides mathjax_url
507 # enable_mathjax=False overrides mathjax_url
507 self.mathjax_url = u''
508 self.mathjax_url = u''
508 else:
509 else:
509 self.log.info("Using MathJax: %s", new)
510 self.log.info("Using MathJax: %s", new)
510
511
511 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
512 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
512 config=True,
513 config=True,
513 help='The notebook manager class to use.')
514 help='The notebook manager class to use.')
514
515
515 trust_xheaders = Bool(False, config=True,
516 trust_xheaders = Bool(False, config=True,
516 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
517 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
517 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
518 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
518 )
519 )
519
520
520 info_file = Unicode()
521 info_file = Unicode()
521
522
522 def _info_file_default(self):
523 def _info_file_default(self):
523 info_file = "nbserver-%s.json"%os.getpid()
524 info_file = "nbserver-%s.json"%os.getpid()
524 return os.path.join(self.profile_dir.security_dir, info_file)
525 return os.path.join(self.profile_dir.security_dir, info_file)
525
526
526 def parse_command_line(self, argv=None):
527 def parse_command_line(self, argv=None):
527 super(NotebookApp, self).parse_command_line(argv)
528 super(NotebookApp, self).parse_command_line(argv)
528
529
529 if self.extra_args:
530 if self.extra_args:
530 arg0 = self.extra_args[0]
531 arg0 = self.extra_args[0]
531 f = os.path.abspath(arg0)
532 f = os.path.abspath(arg0)
532 self.argv.remove(arg0)
533 self.argv.remove(arg0)
533 if not os.path.exists(f):
534 if not os.path.exists(f):
534 self.log.critical("No such file or directory: %s", f)
535 self.log.critical("No such file or directory: %s", f)
535 self.exit(1)
536 self.exit(1)
536 if os.path.isdir(f):
537 if os.path.isdir(f):
537 self.config.FileNotebookManager.notebook_dir = f
538 self.config.FileNotebookManager.notebook_dir = f
538 elif os.path.isfile(f):
539 elif os.path.isfile(f):
539 self.file_to_run = f
540 self.file_to_run = f
540
541
541 def init_kernel_argv(self):
542 def init_kernel_argv(self):
542 """construct the kernel arguments"""
543 """construct the kernel arguments"""
543 # Scrub frontend-specific flags
544 # Scrub frontend-specific flags
544 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
545 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
545 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
546 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
546 self.log.warn('\n '.join([
547 self.log.warn('\n '.join([
547 "Starting all kernels in pylab mode is not recommended,",
548 "Starting all kernels in pylab mode is not recommended,",
548 "and will be disabled in a future release.",
549 "and will be disabled in a future release.",
549 "Please use the %matplotlib magic to enable matplotlib instead.",
550 "Please use the %matplotlib magic to enable matplotlib instead.",
550 "pylab implies many imports, which can have confusing side effects",
551 "pylab implies many imports, which can have confusing side effects",
551 "and harm the reproducibility of your notebooks.",
552 "and harm the reproducibility of your notebooks.",
552 ]))
553 ]))
553 # Kernel should inherit default config file from frontend
554 # Kernel should inherit default config file from frontend
554 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
555 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
555 # Kernel should get *absolute* path to profile directory
556 # Kernel should get *absolute* path to profile directory
556 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
557 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
557
558
558 def init_configurables(self):
559 def init_configurables(self):
559 # force Session default to be secure
560 # force Session default to be secure
560 default_secure(self.config)
561 default_secure(self.config)
561 self.kernel_manager = MappingKernelManager(
562 self.kernel_manager = MappingKernelManager(
562 parent=self, log=self.log, kernel_argv=self.kernel_argv,
563 parent=self, log=self.log, kernel_argv=self.kernel_argv,
563 connection_dir = self.profile_dir.security_dir,
564 connection_dir = self.profile_dir.security_dir,
564 )
565 )
565 kls = import_item(self.notebook_manager_class)
566 kls = import_item(self.notebook_manager_class)
566 self.notebook_manager = kls(parent=self, log=self.log)
567 self.notebook_manager = kls(parent=self, log=self.log)
567 self.session_manager = SessionManager(parent=self, log=self.log)
568 self.session_manager = SessionManager(parent=self, log=self.log)
568 self.cluster_manager = ClusterManager(parent=self, log=self.log)
569 self.cluster_manager = ClusterManager(parent=self, log=self.log)
569 self.cluster_manager.update_profiles()
570 self.cluster_manager.update_profiles()
570
571
571 def init_logging(self):
572 def init_logging(self):
572 # This prevents double log messages because tornado use a root logger that
573 # This prevents double log messages because tornado use a root logger that
573 # self.log is a child of. The logging module dipatches log messages to a log
574 # self.log is a child of. The logging module dipatches log messages to a log
574 # and all of its ancenstors until propagate is set to False.
575 # and all of its ancenstors until propagate is set to False.
575 self.log.propagate = False
576 self.log.propagate = False
576
577
577 # hook up tornado 3's loggers to our app handlers
578 # hook up tornado 3's loggers to our app handlers
578 for name in ('access', 'application', 'general'):
579 for name in ('access', 'application', 'general'):
579 logger = logging.getLogger('tornado.%s' % name)
580 logger = logging.getLogger('tornado.%s' % name)
580 logger.parent = self.log
581 logger.parent = self.log
581 logger.setLevel(self.log.level)
582 logger.setLevel(self.log.level)
582
583
583 def init_webapp(self):
584 def init_webapp(self):
584 """initialize tornado webapp and httpserver"""
585 """initialize tornado webapp and httpserver"""
585 self.web_app = NotebookWebApplication(
586 self.web_app = NotebookWebApplication(
586 self, self.kernel_manager, self.notebook_manager,
587 self, self.kernel_manager, self.notebook_manager,
587 self.cluster_manager, self.session_manager,
588 self.cluster_manager, self.session_manager,
588 self.log, self.base_project_url, self.webapp_settings
589 self.log, self.base_project_url, self.webapp_settings
589 )
590 )
590 if self.certfile:
591 if self.certfile:
591 ssl_options = dict(certfile=self.certfile)
592 ssl_options = dict(certfile=self.certfile)
592 if self.keyfile:
593 if self.keyfile:
593 ssl_options['keyfile'] = self.keyfile
594 ssl_options['keyfile'] = self.keyfile
594 else:
595 else:
595 ssl_options = None
596 ssl_options = None
596 self.web_app.password = self.password
597 self.web_app.password = self.password
597 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
598 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
598 xheaders=self.trust_xheaders)
599 xheaders=self.trust_xheaders)
599 if not self.ip:
600 if not self.ip:
600 warning = "WARNING: The notebook server is listening on all IP addresses"
601 warning = "WARNING: The notebook server is listening on all IP addresses"
601 if ssl_options is None:
602 if ssl_options is None:
602 self.log.critical(warning + " and not using encryption. This "
603 self.log.critical(warning + " and not using encryption. This "
603 "is not recommended.")
604 "is not recommended.")
604 if not self.password:
605 if not self.password:
605 self.log.critical(warning + " and not using authentication. "
606 self.log.critical(warning + " and not using authentication. "
606 "This is highly insecure and not recommended.")
607 "This is highly insecure and not recommended.")
607 success = None
608 success = None
608 for port in random_ports(self.port, self.port_retries+1):
609 for port in random_ports(self.port, self.port_retries+1):
609 try:
610 try:
610 self.http_server.listen(port, self.ip)
611 self.http_server.listen(port, self.ip)
611 except socket.error as e:
612 except socket.error as e:
612 if e.errno == errno.EADDRINUSE:
613 if e.errno == errno.EADDRINUSE:
613 self.log.info('The port %i is already in use, trying another random port.' % port)
614 self.log.info('The port %i is already in use, trying another random port.' % port)
614 continue
615 continue
615 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
616 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
616 self.log.warn("Permission to listen on port %i denied" % port)
617 self.log.warn("Permission to listen on port %i denied" % port)
617 continue
618 continue
618 else:
619 else:
619 raise
620 raise
620 else:
621 else:
621 self.port = port
622 self.port = port
622 success = True
623 success = True
623 break
624 break
624 if not success:
625 if not success:
625 self.log.critical('ERROR: the notebook server could not be started because '
626 self.log.critical('ERROR: the notebook server could not be started because '
626 'no available port could be found.')
627 'no available port could be found.')
627 self.exit(1)
628 self.exit(1)
628
629
629 @property
630 @property
630 def display_url(self):
631 def display_url(self):
631 ip = self.ip if self.ip else '[all ip addresses on your system]'
632 ip = self.ip if self.ip else '[all ip addresses on your system]'
632 return self._url(ip)
633 return self._url(ip)
633
634
634 @property
635 @property
635 def connection_url(self):
636 def connection_url(self):
636 ip = self.ip if self.ip else localhost()
637 ip = self.ip if self.ip else localhost()
637 return self._url(ip)
638 return self._url(ip)
638
639
639 def _url(self, ip):
640 def _url(self, ip):
640 proto = 'https' if self.certfile else 'http'
641 proto = 'https' if self.certfile else 'http'
641 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
642 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
642
643
643 def init_signal(self):
644 def init_signal(self):
644 if not sys.platform.startswith('win'):
645 if not sys.platform.startswith('win'):
645 signal.signal(signal.SIGINT, self._handle_sigint)
646 signal.signal(signal.SIGINT, self._handle_sigint)
646 signal.signal(signal.SIGTERM, self._signal_stop)
647 signal.signal(signal.SIGTERM, self._signal_stop)
647 if hasattr(signal, 'SIGUSR1'):
648 if hasattr(signal, 'SIGUSR1'):
648 # Windows doesn't support SIGUSR1
649 # Windows doesn't support SIGUSR1
649 signal.signal(signal.SIGUSR1, self._signal_info)
650 signal.signal(signal.SIGUSR1, self._signal_info)
650 if hasattr(signal, 'SIGINFO'):
651 if hasattr(signal, 'SIGINFO'):
651 # only on BSD-based systems
652 # only on BSD-based systems
652 signal.signal(signal.SIGINFO, self._signal_info)
653 signal.signal(signal.SIGINFO, self._signal_info)
653
654
654 def _handle_sigint(self, sig, frame):
655 def _handle_sigint(self, sig, frame):
655 """SIGINT handler spawns confirmation dialog"""
656 """SIGINT handler spawns confirmation dialog"""
656 # register more forceful signal handler for ^C^C case
657 # register more forceful signal handler for ^C^C case
657 signal.signal(signal.SIGINT, self._signal_stop)
658 signal.signal(signal.SIGINT, self._signal_stop)
658 # request confirmation dialog in bg thread, to avoid
659 # request confirmation dialog in bg thread, to avoid
659 # blocking the App
660 # blocking the App
660 thread = threading.Thread(target=self._confirm_exit)
661 thread = threading.Thread(target=self._confirm_exit)
661 thread.daemon = True
662 thread.daemon = True
662 thread.start()
663 thread.start()
663
664
664 def _restore_sigint_handler(self):
665 def _restore_sigint_handler(self):
665 """callback for restoring original SIGINT handler"""
666 """callback for restoring original SIGINT handler"""
666 signal.signal(signal.SIGINT, self._handle_sigint)
667 signal.signal(signal.SIGINT, self._handle_sigint)
667
668
668 def _confirm_exit(self):
669 def _confirm_exit(self):
669 """confirm shutdown on ^C
670 """confirm shutdown on ^C
670
671
671 A second ^C, or answering 'y' within 5s will cause shutdown,
672 A second ^C, or answering 'y' within 5s will cause shutdown,
672 otherwise original SIGINT handler will be restored.
673 otherwise original SIGINT handler will be restored.
673
674
674 This doesn't work on Windows.
675 This doesn't work on Windows.
675 """
676 """
676 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
677 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
677 time.sleep(0.1)
678 time.sleep(0.1)
678 info = self.log.info
679 info = self.log.info
679 info('interrupted')
680 info('interrupted')
680 print(self.notebook_info())
681 print(self.notebook_info())
681 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
682 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
682 sys.stdout.flush()
683 sys.stdout.flush()
683 r,w,x = select.select([sys.stdin], [], [], 5)
684 r,w,x = select.select([sys.stdin], [], [], 5)
684 if r:
685 if r:
685 line = sys.stdin.readline()
686 line = sys.stdin.readline()
686 if line.lower().startswith('y'):
687 if line.lower().startswith('y'):
687 self.log.critical("Shutdown confirmed")
688 self.log.critical("Shutdown confirmed")
688 ioloop.IOLoop.instance().stop()
689 ioloop.IOLoop.instance().stop()
689 return
690 return
690 else:
691 else:
691 print("No answer for 5s:", end=' ')
692 print("No answer for 5s:", end=' ')
692 print("resuming operation...")
693 print("resuming operation...")
693 # no answer, or answer is no:
694 # no answer, or answer is no:
694 # set it back to original SIGINT handler
695 # set it back to original SIGINT handler
695 # use IOLoop.add_callback because signal.signal must be called
696 # use IOLoop.add_callback because signal.signal must be called
696 # from main thread
697 # from main thread
697 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
698 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
698
699
699 def _signal_stop(self, sig, frame):
700 def _signal_stop(self, sig, frame):
700 self.log.critical("received signal %s, stopping", sig)
701 self.log.critical("received signal %s, stopping", sig)
701 ioloop.IOLoop.instance().stop()
702 ioloop.IOLoop.instance().stop()
702
703
703 def _signal_info(self, sig, frame):
704 def _signal_info(self, sig, frame):
704 print(self.notebook_info())
705 print(self.notebook_info())
705
706
706 def init_components(self):
707 def init_components(self):
707 """Check the components submodule, and warn if it's unclean"""
708 """Check the components submodule, and warn if it's unclean"""
708 status = submodule.check_submodule_status()
709 status = submodule.check_submodule_status()
709 if status == 'missing':
710 if status == 'missing':
710 self.log.warn("components submodule missing, running `git submodule update`")
711 self.log.warn("components submodule missing, running `git submodule update`")
711 submodule.update_submodules(submodule.ipython_parent())
712 submodule.update_submodules(submodule.ipython_parent())
712 elif status == 'unclean':
713 elif status == 'unclean':
713 self.log.warn("components submodule unclean, you may see 404s on static/components")
714 self.log.warn("components submodule unclean, you may see 404s on static/components")
714 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
715 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
715
716
716 @catch_config_error
717 @catch_config_error
717 def initialize(self, argv=None):
718 def initialize(self, argv=None):
718 super(NotebookApp, self).initialize(argv)
719 super(NotebookApp, self).initialize(argv)
719 self.init_logging()
720 self.init_logging()
720 self.init_kernel_argv()
721 self.init_kernel_argv()
721 self.init_configurables()
722 self.init_configurables()
722 self.init_components()
723 self.init_components()
723 self.init_webapp()
724 self.init_webapp()
724 self.init_signal()
725 self.init_signal()
725
726
726 def cleanup_kernels(self):
727 def cleanup_kernels(self):
727 """Shutdown all kernels.
728 """Shutdown all kernels.
728
729
729 The kernels will shutdown themselves when this process no longer exists,
730 The kernels will shutdown themselves when this process no longer exists,
730 but explicit shutdown allows the KernelManagers to cleanup the connection files.
731 but explicit shutdown allows the KernelManagers to cleanup the connection files.
731 """
732 """
732 self.log.info('Shutting down kernels')
733 self.log.info('Shutting down kernels')
733 self.kernel_manager.shutdown_all()
734 self.kernel_manager.shutdown_all()
734
735
735 def notebook_info(self):
736 def notebook_info(self):
736 "Return the current working directory and the server url information"
737 "Return the current working directory and the server url information"
737 info = self.notebook_manager.info_string() + "\n"
738 info = self.notebook_manager.info_string() + "\n"
738 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
739 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
739 return info + "The IPython Notebook is running at: %s" % self.display_url
740 return info + "The IPython Notebook is running at: %s" % self.display_url
740
741
741 def server_info(self):
742 def server_info(self):
742 """Return a JSONable dict of information about this server."""
743 """Return a JSONable dict of information about this server."""
743 return {'url': self.connection_url,
744 return {'url': self.connection_url,
744 'hostname': self.ip if self.ip else 'localhost',
745 'hostname': self.ip if self.ip else 'localhost',
745 'port': self.port,
746 'port': self.port,
746 'secure': bool(self.certfile),
747 'secure': bool(self.certfile),
747 'base_project_url': self.base_project_url,
748 'base_project_url': self.base_project_url,
748 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
749 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
749 }
750 }
750
751
751 def write_server_info_file(self):
752 def write_server_info_file(self):
752 """Write the result of server_info() to the JSON file info_file."""
753 """Write the result of server_info() to the JSON file info_file."""
753 with open(self.info_file, 'w') as f:
754 with open(self.info_file, 'w') as f:
754 json.dump(self.server_info(), f, indent=2)
755 json.dump(self.server_info(), f, indent=2)
755
756
756 def remove_server_info_file(self):
757 def remove_server_info_file(self):
757 """Remove the nbserver-<pid>.json file created for this server.
758 """Remove the nbserver-<pid>.json file created for this server.
758
759
759 Ignores the error raised when the file has already been removed.
760 Ignores the error raised when the file has already been removed.
760 """
761 """
761 try:
762 try:
762 os.unlink(self.info_file)
763 os.unlink(self.info_file)
763 except OSError as e:
764 except OSError as e:
764 if e.errno != errno.ENOENT:
765 if e.errno != errno.ENOENT:
765 raise
766 raise
766
767
767 def start(self):
768 def start(self):
768 """ Start the IPython Notebook server app, after initialization
769 """ Start the IPython Notebook server app, after initialization
769
770
770 This method takes no arguments so all configuration and initialization
771 This method takes no arguments so all configuration and initialization
771 must be done prior to calling this method."""
772 must be done prior to calling this method."""
772 if self.subapp is not None:
773 if self.subapp is not None:
773 return self.subapp.start()
774 return self.subapp.start()
774
775
775 info = self.log.info
776 info = self.log.info
776 for line in self.notebook_info().split("\n"):
777 for line in self.notebook_info().split("\n"):
777 info(line)
778 info(line)
778 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
779 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
779
780
780 self.write_server_info_file()
781 self.write_server_info_file()
781
782
782 if self.open_browser or self.file_to_run:
783 if self.open_browser or self.file_to_run:
783 try:
784 try:
784 browser = webbrowser.get(self.browser or None)
785 browser = webbrowser.get(self.browser or None)
785 except webbrowser.Error as e:
786 except webbrowser.Error as e:
786 self.log.warn('No web browser found: %s.' % e)
787 self.log.warn('No web browser found: %s.' % e)
787 browser = None
788 browser = None
788
789
789 f = self.file_to_run
790 f = self.file_to_run
790 if f:
791 if f:
791 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
792 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
792 if f.startswith(nbdir):
793 if f.startswith(nbdir):
793 f = f[len(nbdir):]
794 f = f[len(nbdir):]
794 else:
795 else:
795 self.log.warn(
796 self.log.warn(
796 "Probably won't be able to open notebook %s "
797 "Probably won't be able to open notebook %s "
797 "because it is not in notebook_dir %s",
798 "because it is not in notebook_dir %s",
798 f, nbdir,
799 f, nbdir,
799 )
800 )
800
801
801 if os.path.isfile(self.file_to_run):
802 if os.path.isfile(self.file_to_run):
802 url = url_path_join('notebooks', f)
803 url = url_path_join('notebooks', f)
803 else:
804 else:
804 url = url_path_join('tree', f)
805 url = url_path_join('tree', f)
805 if browser:
806 if browser:
806 b = lambda : browser.open("%s%s" % (self.connection_url, url),
807 b = lambda : browser.open("%s%s" % (self.connection_url, url),
807 new=2)
808 new=2)
808 threading.Thread(target=b).start()
809 threading.Thread(target=b).start()
809 try:
810 try:
810 ioloop.IOLoop.instance().start()
811 ioloop.IOLoop.instance().start()
811 except KeyboardInterrupt:
812 except KeyboardInterrupt:
812 info("Interrupted...")
813 info("Interrupted...")
813 finally:
814 finally:
814 self.cleanup_kernels()
815 self.cleanup_kernels()
815 self.remove_server_info_file()
816 self.remove_server_info_file()
816
817
817
818
818 def list_running_servers(profile='default'):
819 def list_running_servers(profile='default'):
819 """Iterate over the server info files of running notebook servers.
820 """Iterate over the server info files of running notebook servers.
820
821
821 Given a profile name, find nbserver-* files in the security directory of
822 Given a profile name, find nbserver-* files in the security directory of
822 that profile, and yield dicts of their information, each one pertaining to
823 that profile, and yield dicts of their information, each one pertaining to
823 a currently running notebook server instance.
824 a currently running notebook server instance.
824 """
825 """
825 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
826 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
826 for file in os.listdir(pd.security_dir):
827 for file in os.listdir(pd.security_dir):
827 if file.startswith('nbserver-'):
828 if file.startswith('nbserver-'):
828 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
829 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
829 yield json.load(f)
830 yield json.load(f)
830
831
831 #-----------------------------------------------------------------------------
832 #-----------------------------------------------------------------------------
832 # Main entry point
833 # Main entry point
833 #-----------------------------------------------------------------------------
834 #-----------------------------------------------------------------------------
834
835
835 launch_new_instance = NotebookApp.launch_instance
836 launch_new_instance = NotebookApp.launch_instance
836
837
@@ -1,559 +1,559 b''
1 """Utilities for connecting to kernels
1 """Utilities for connecting to kernels
2
2
3 Authors:
3 Authors:
4
4
5 * Min Ragan-Kelley
5 * Min Ragan-Kelley
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2013 The IPython Development Team
10 # Copyright (C) 2013 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 from __future__ import absolute_import
20 from __future__ import absolute_import
21
21
22 import glob
22 import glob
23 import json
23 import json
24 import os
24 import os
25 import socket
25 import socket
26 import sys
26 import sys
27 from getpass import getpass
27 from getpass import getpass
28 from subprocess import Popen, PIPE
28 from subprocess import Popen, PIPE
29 import tempfile
29 import tempfile
30
30
31 import zmq
31 import zmq
32
32
33 # external imports
33 # external imports
34 from IPython.external.ssh import tunnel
34 from IPython.external.ssh import tunnel
35
35
36 # IPython imports
36 # IPython imports
37 from IPython.config import Configurable
37 from IPython.config import Configurable
38 from IPython.core.profiledir import ProfileDir
38 from IPython.core.profiledir import ProfileDir
39 from IPython.utils.localinterfaces import localhost
39 from IPython.utils.localinterfaces import localhost
40 from IPython.utils.path import filefind, get_ipython_dir
40 from IPython.utils.path import filefind, get_ipython_dir
41 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
41 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
42 string_types)
42 string_types)
43 from IPython.utils.traitlets import (
43 from IPython.utils.traitlets import (
44 Bool, Integer, Unicode, CaselessStrEnum,
44 Bool, Integer, Unicode, CaselessStrEnum,
45 )
45 )
46
46
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # Working with Connection Files
49 # Working with Connection Files
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
52 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
53 control_port=0, ip='', key=b'', transport='tcp',
53 control_port=0, ip='', key=b'', transport='tcp',
54 signature_scheme='hmac-sha256',
54 signature_scheme='hmac-sha256',
55 ):
55 ):
56 """Generates a JSON config file, including the selection of random ports.
56 """Generates a JSON config file, including the selection of random ports.
57
57
58 Parameters
58 Parameters
59 ----------
59 ----------
60
60
61 fname : unicode
61 fname : unicode
62 The path to the file to write
62 The path to the file to write
63
63
64 shell_port : int, optional
64 shell_port : int, optional
65 The port to use for ROUTER (shell) channel.
65 The port to use for ROUTER (shell) channel.
66
66
67 iopub_port : int, optional
67 iopub_port : int, optional
68 The port to use for the SUB channel.
68 The port to use for the SUB channel.
69
69
70 stdin_port : int, optional
70 stdin_port : int, optional
71 The port to use for the ROUTER (raw input) channel.
71 The port to use for the ROUTER (raw input) channel.
72
72
73 control_port : int, optional
73 control_port : int, optional
74 The port to use for the ROUTER (control) channel.
74 The port to use for the ROUTER (control) channel.
75
75
76 hb_port : int, optional
76 hb_port : int, optional
77 The port to use for the heartbeat REP channel.
77 The port to use for the heartbeat REP channel.
78
78
79 ip : str, optional
79 ip : str, optional
80 The ip address the kernel will bind to.
80 The ip address the kernel will bind to.
81
81
82 key : str, optional
82 key : str, optional
83 The Session key used for message authentication.
83 The Session key used for message authentication.
84
84
85 signature_scheme : str, optional
85 signature_scheme : str, optional
86 The scheme used for message authentication.
86 The scheme used for message authentication.
87 This has the form 'digest-hash', where 'digest'
87 This has the form 'digest-hash', where 'digest'
88 is the scheme used for digests, and 'hash' is the name of the hash function
88 is the scheme used for digests, and 'hash' is the name of the hash function
89 used by the digest scheme.
89 used by the digest scheme.
90 Currently, 'hmac' is the only supported digest scheme,
90 Currently, 'hmac' is the only supported digest scheme,
91 and 'sha256' is the default hash function.
91 and 'sha256' is the default hash function.
92
92
93 """
93 """
94 if not ip:
94 if not ip:
95 ip = localhost()
95 ip = localhost()
96 # default to temporary connector file
96 # default to temporary connector file
97 if not fname:
97 if not fname:
98 fname = tempfile.mktemp('.json')
98 fname = tempfile.mktemp('.json')
99
99
100 # Find open ports as necessary.
100 # Find open ports as necessary.
101
101
102 ports = []
102 ports = []
103 ports_needed = int(shell_port <= 0) + \
103 ports_needed = int(shell_port <= 0) + \
104 int(iopub_port <= 0) + \
104 int(iopub_port <= 0) + \
105 int(stdin_port <= 0) + \
105 int(stdin_port <= 0) + \
106 int(control_port <= 0) + \
106 int(control_port <= 0) + \
107 int(hb_port <= 0)
107 int(hb_port <= 0)
108 if transport == 'tcp':
108 if transport == 'tcp':
109 for i in range(ports_needed):
109 for i in range(ports_needed):
110 sock = socket.socket()
110 sock = socket.socket()
111 sock.bind(('', 0))
111 sock.bind(('', 0))
112 ports.append(sock)
112 ports.append(sock)
113 for i, sock in enumerate(ports):
113 for i, sock in enumerate(ports):
114 port = sock.getsockname()[1]
114 port = sock.getsockname()[1]
115 sock.close()
115 sock.close()
116 ports[i] = port
116 ports[i] = port
117 else:
117 else:
118 N = 1
118 N = 1
119 for i in range(ports_needed):
119 for i in range(ports_needed):
120 while os.path.exists("%s-%s" % (ip, str(N))):
120 while os.path.exists("%s-%s" % (ip, str(N))):
121 N += 1
121 N += 1
122 ports.append(N)
122 ports.append(N)
123 N += 1
123 N += 1
124 if shell_port <= 0:
124 if shell_port <= 0:
125 shell_port = ports.pop(0)
125 shell_port = ports.pop(0)
126 if iopub_port <= 0:
126 if iopub_port <= 0:
127 iopub_port = ports.pop(0)
127 iopub_port = ports.pop(0)
128 if stdin_port <= 0:
128 if stdin_port <= 0:
129 stdin_port = ports.pop(0)
129 stdin_port = ports.pop(0)
130 if control_port <= 0:
130 if control_port <= 0:
131 control_port = ports.pop(0)
131 control_port = ports.pop(0)
132 if hb_port <= 0:
132 if hb_port <= 0:
133 hb_port = ports.pop(0)
133 hb_port = ports.pop(0)
134
134
135 cfg = dict( shell_port=shell_port,
135 cfg = dict( shell_port=shell_port,
136 iopub_port=iopub_port,
136 iopub_port=iopub_port,
137 stdin_port=stdin_port,
137 stdin_port=stdin_port,
138 control_port=control_port,
138 control_port=control_port,
139 hb_port=hb_port,
139 hb_port=hb_port,
140 )
140 )
141 cfg['ip'] = ip
141 cfg['ip'] = ip
142 cfg['key'] = bytes_to_str(key)
142 cfg['key'] = bytes_to_str(key)
143 cfg['transport'] = transport
143 cfg['transport'] = transport
144 cfg['signature_scheme'] = signature_scheme
144 cfg['signature_scheme'] = signature_scheme
145
145
146 with open(fname, 'w') as f:
146 with open(fname, 'w') as f:
147 f.write(json.dumps(cfg, indent=2))
147 f.write(json.dumps(cfg, indent=2))
148
148
149 return fname, cfg
149 return fname, cfg
150
150
151
151
152 def get_connection_file(app=None):
152 def get_connection_file(app=None):
153 """Return the path to the connection file of an app
153 """Return the path to the connection file of an app
154
154
155 Parameters
155 Parameters
156 ----------
156 ----------
157 app : IPKernelApp instance [optional]
157 app : IPKernelApp instance [optional]
158 If unspecified, the currently running app will be used
158 If unspecified, the currently running app will be used
159 """
159 """
160 if app is None:
160 if app is None:
161 from IPython.kernel.zmq.kernelapp import IPKernelApp
161 from IPython.kernel.zmq.kernelapp import IPKernelApp
162 if not IPKernelApp.initialized():
162 if not IPKernelApp.initialized():
163 raise RuntimeError("app not specified, and not in a running Kernel")
163 raise RuntimeError("app not specified, and not in a running Kernel")
164
164
165 app = IPKernelApp.instance()
165 app = IPKernelApp.instance()
166 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
166 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
167
167
168
168
169 def find_connection_file(filename, profile=None):
169 def find_connection_file(filename, profile=None):
170 """find a connection file, and return its absolute path.
170 """find a connection file, and return its absolute path.
171
171
172 The current working directory and the profile's security
172 The current working directory and the profile's security
173 directory will be searched for the file if it is not given by
173 directory will be searched for the file if it is not given by
174 absolute path.
174 absolute path.
175
175
176 If profile is unspecified, then the current running application's
176 If profile is unspecified, then the current running application's
177 profile will be used, or 'default', if not run from IPython.
177 profile will be used, or 'default', if not run from IPython.
178
178
179 If the argument does not match an existing file, it will be interpreted as a
179 If the argument does not match an existing file, it will be interpreted as a
180 fileglob, and the matching file in the profile's security dir with
180 fileglob, and the matching file in the profile's security dir with
181 the latest access time will be used.
181 the latest access time will be used.
182
182
183 Parameters
183 Parameters
184 ----------
184 ----------
185 filename : str
185 filename : str
186 The connection file or fileglob to search for.
186 The connection file or fileglob to search for.
187 profile : str [optional]
187 profile : str [optional]
188 The name of the profile to use when searching for the connection file,
188 The name of the profile to use when searching for the connection file,
189 if different from the current IPython session or 'default'.
189 if different from the current IPython session or 'default'.
190
190
191 Returns
191 Returns
192 -------
192 -------
193 str : The absolute path of the connection file.
193 str : The absolute path of the connection file.
194 """
194 """
195 from IPython.core.application import BaseIPythonApplication as IPApp
195 from IPython.core.application import BaseIPythonApplication as IPApp
196 try:
196 try:
197 # quick check for absolute path, before going through logic
197 # quick check for absolute path, before going through logic
198 return filefind(filename)
198 return filefind(filename)
199 except IOError:
199 except IOError:
200 pass
200 pass
201
201
202 if profile is None:
202 if profile is None:
203 # profile unspecified, check if running from an IPython app
203 # profile unspecified, check if running from an IPython app
204 if IPApp.initialized():
204 if IPApp.initialized():
205 app = IPApp.instance()
205 app = IPApp.instance()
206 profile_dir = app.profile_dir
206 profile_dir = app.profile_dir
207 else:
207 else:
208 # not running in IPython, use default profile
208 # not running in IPython, use default profile
209 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
209 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
210 else:
210 else:
211 # find profiledir by profile name:
211 # find profiledir by profile name:
212 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
212 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
213 security_dir = profile_dir.security_dir
213 security_dir = profile_dir.security_dir
214
214
215 try:
215 try:
216 # first, try explicit name
216 # first, try explicit name
217 return filefind(filename, ['.', security_dir])
217 return filefind(filename, ['.', security_dir])
218 except IOError:
218 except IOError:
219 pass
219 pass
220
220
221 # not found by full name
221 # not found by full name
222
222
223 if '*' in filename:
223 if '*' in filename:
224 # given as a glob already
224 # given as a glob already
225 pat = filename
225 pat = filename
226 else:
226 else:
227 # accept any substring match
227 # accept any substring match
228 pat = '*%s*' % filename
228 pat = '*%s*' % filename
229 matches = glob.glob( os.path.join(security_dir, pat) )
229 matches = glob.glob( os.path.join(security_dir, pat) )
230 if not matches:
230 if not matches:
231 raise IOError("Could not find %r in %r" % (filename, security_dir))
231 raise IOError("Could not find %r in %r" % (filename, security_dir))
232 elif len(matches) == 1:
232 elif len(matches) == 1:
233 return matches[0]
233 return matches[0]
234 else:
234 else:
235 # get most recent match, by access time:
235 # get most recent match, by access time:
236 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
236 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
237
237
238
238
239 def get_connection_info(connection_file=None, unpack=False, profile=None):
239 def get_connection_info(connection_file=None, unpack=False, profile=None):
240 """Return the connection information for the current Kernel.
240 """Return the connection information for the current Kernel.
241
241
242 Parameters
242 Parameters
243 ----------
243 ----------
244 connection_file : str [optional]
244 connection_file : str [optional]
245 The connection file to be used. Can be given by absolute path, or
245 The connection file to be used. Can be given by absolute path, or
246 IPython will search in the security directory of a given profile.
246 IPython will search in the security directory of a given profile.
247 If run from IPython,
247 If run from IPython,
248
248
249 If unspecified, the connection file for the currently running
249 If unspecified, the connection file for the currently running
250 IPython Kernel will be used, which is only allowed from inside a kernel.
250 IPython Kernel will be used, which is only allowed from inside a kernel.
251 unpack : bool [default: False]
251 unpack : bool [default: False]
252 if True, return the unpacked dict, otherwise just the string contents
252 if True, return the unpacked dict, otherwise just the string contents
253 of the file.
253 of the file.
254 profile : str [optional]
254 profile : str [optional]
255 The name of the profile to use when searching for the connection file,
255 The name of the profile to use when searching for the connection file,
256 if different from the current IPython session or 'default'.
256 if different from the current IPython session or 'default'.
257
257
258
258
259 Returns
259 Returns
260 -------
260 -------
261 The connection dictionary of the current kernel, as string or dict,
261 The connection dictionary of the current kernel, as string or dict,
262 depending on `unpack`.
262 depending on `unpack`.
263 """
263 """
264 if connection_file is None:
264 if connection_file is None:
265 # get connection file from current kernel
265 # get connection file from current kernel
266 cf = get_connection_file()
266 cf = get_connection_file()
267 else:
267 else:
268 # connection file specified, allow shortnames:
268 # connection file specified, allow shortnames:
269 cf = find_connection_file(connection_file, profile=profile)
269 cf = find_connection_file(connection_file, profile=profile)
270
270
271 with open(cf) as f:
271 with open(cf) as f:
272 info = f.read()
272 info = f.read()
273
273
274 if unpack:
274 if unpack:
275 info = json.loads(info)
275 info = json.loads(info)
276 # ensure key is bytes:
276 # ensure key is bytes:
277 info['key'] = str_to_bytes(info.get('key', ''))
277 info['key'] = str_to_bytes(info.get('key', ''))
278 return info
278 return info
279
279
280
280
281 def connect_qtconsole(connection_file=None, argv=None, profile=None):
281 def connect_qtconsole(connection_file=None, argv=None, profile=None):
282 """Connect a qtconsole to the current kernel.
282 """Connect a qtconsole to the current kernel.
283
283
284 This is useful for connecting a second qtconsole to a kernel, or to a
284 This is useful for connecting a second qtconsole to a kernel, or to a
285 local notebook.
285 local notebook.
286
286
287 Parameters
287 Parameters
288 ----------
288 ----------
289 connection_file : str [optional]
289 connection_file : str [optional]
290 The connection file to be used. Can be given by absolute path, or
290 The connection file to be used. Can be given by absolute path, or
291 IPython will search in the security directory of a given profile.
291 IPython will search in the security directory of a given profile.
292 If run from IPython,
292 If run from IPython,
293
293
294 If unspecified, the connection file for the currently running
294 If unspecified, the connection file for the currently running
295 IPython Kernel will be used, which is only allowed from inside a kernel.
295 IPython Kernel will be used, which is only allowed from inside a kernel.
296 argv : list [optional]
296 argv : list [optional]
297 Any extra args to be passed to the console.
297 Any extra args to be passed to the console.
298 profile : str [optional]
298 profile : str [optional]
299 The name of the profile to use when searching for the connection file,
299 The name of the profile to use when searching for the connection file,
300 if different from the current IPython session or 'default'.
300 if different from the current IPython session or 'default'.
301
301
302
302
303 Returns
303 Returns
304 -------
304 -------
305 subprocess.Popen instance running the qtconsole frontend
305 subprocess.Popen instance running the qtconsole frontend
306 """
306 """
307 argv = [] if argv is None else argv
307 argv = [] if argv is None else argv
308
308
309 if connection_file is None:
309 if connection_file is None:
310 # get connection file from current kernel
310 # get connection file from current kernel
311 cf = get_connection_file()
311 cf = get_connection_file()
312 else:
312 else:
313 cf = find_connection_file(connection_file, profile=profile)
313 cf = find_connection_file(connection_file, profile=profile)
314
314
315 cmd = ';'.join([
315 cmd = ';'.join([
316 "from IPython.qt.console import qtconsoleapp",
316 "from IPython.qt.console import qtconsoleapp",
317 "qtconsoleapp.main()"
317 "qtconsoleapp.main()"
318 ])
318 ])
319
319
320 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
320 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
321 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
321 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
322 )
322 )
323
323
324
324
325 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
325 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
326 """tunnel connections to a kernel via ssh
326 """tunnel connections to a kernel via ssh
327
327
328 This will open four SSH tunnels from localhost on this machine to the
328 This will open four SSH tunnels from localhost on this machine to the
329 ports associated with the kernel. They can be either direct
329 ports associated with the kernel. They can be either direct
330 localhost-localhost tunnels, or if an intermediate server is necessary,
330 localhost-localhost tunnels, or if an intermediate server is necessary,
331 the kernel must be listening on a public IP.
331 the kernel must be listening on a public IP.
332
332
333 Parameters
333 Parameters
334 ----------
334 ----------
335 connection_info : dict or str (path)
335 connection_info : dict or str (path)
336 Either a connection dict, or the path to a JSON connection file
336 Either a connection dict, or the path to a JSON connection file
337 sshserver : str
337 sshserver : str
338 The ssh sever to use to tunnel to the kernel. Can be a full
338 The ssh sever to use to tunnel to the kernel. Can be a full
339 `user@server:port` string. ssh config aliases are respected.
339 `user@server:port` string. ssh config aliases are respected.
340 sshkey : str [optional]
340 sshkey : str [optional]
341 Path to file containing ssh key to use for authentication.
341 Path to file containing ssh key to use for authentication.
342 Only necessary if your ssh config does not already associate
342 Only necessary if your ssh config does not already associate
343 a keyfile with the host.
343 a keyfile with the host.
344
344
345 Returns
345 Returns
346 -------
346 -------
347
347
348 (shell, iopub, stdin, hb) : ints
348 (shell, iopub, stdin, hb) : ints
349 The four ports on localhost that have been forwarded to the kernel.
349 The four ports on localhost that have been forwarded to the kernel.
350 """
350 """
351 if isinstance(connection_info, string_types):
351 if isinstance(connection_info, string_types):
352 # it's a path, unpack it
352 # it's a path, unpack it
353 with open(connection_info) as f:
353 with open(connection_info) as f:
354 connection_info = json.loads(f.read())
354 connection_info = json.loads(f.read())
355
355
356 cf = connection_info
356 cf = connection_info
357
357
358 lports = tunnel.select_random_ports(4)
358 lports = tunnel.select_random_ports(4)
359 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
359 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
360
360
361 remote_ip = cf['ip']
361 remote_ip = cf['ip']
362
362
363 if tunnel.try_passwordless_ssh(sshserver, sshkey):
363 if tunnel.try_passwordless_ssh(sshserver, sshkey):
364 password=False
364 password=False
365 else:
365 else:
366 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
366 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
367
367
368 for lp,rp in zip(lports, rports):
368 for lp,rp in zip(lports, rports):
369 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
369 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
370
370
371 return tuple(lports)
371 return tuple(lports)
372
372
373
373
374 #-----------------------------------------------------------------------------
374 #-----------------------------------------------------------------------------
375 # Mixin for classes that work with connection files
375 # Mixin for classes that work with connection files
376 #-----------------------------------------------------------------------------
376 #-----------------------------------------------------------------------------
377
377
378 channel_socket_types = {
378 channel_socket_types = {
379 'hb' : zmq.REQ,
379 'hb' : zmq.REQ,
380 'shell' : zmq.DEALER,
380 'shell' : zmq.DEALER,
381 'iopub' : zmq.SUB,
381 'iopub' : zmq.SUB,
382 'stdin' : zmq.DEALER,
382 'stdin' : zmq.DEALER,
383 'control': zmq.DEALER,
383 'control': zmq.DEALER,
384 }
384 }
385
385
386 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
386 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
387
387
388 class ConnectionFileMixin(Configurable):
388 class ConnectionFileMixin(Configurable):
389 """Mixin for configurable classes that work with connection files"""
389 """Mixin for configurable classes that work with connection files"""
390
390
391 # The addresses for the communication channels
391 # The addresses for the communication channels
392 connection_file = Unicode('')
392 connection_file = Unicode('')
393 _connection_file_written = Bool(False)
393 _connection_file_written = Bool(False)
394
394
395 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
395 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
396
396
397 ip = Unicode(config=True,
397 ip = Unicode(config=True,
398 help="""Set the kernel\'s IP address [default localhost].
398 help="""Set the kernel\'s IP address [default localhost].
399 If the IP address is something other than localhost, then
399 If the IP address is something other than localhost, then
400 Consoles on other machines will be able to connect
400 Consoles on other machines will be able to connect
401 to the Kernel, so be careful!"""
401 to the Kernel, so be careful!"""
402 )
402 )
403
403
404 def _ip_default(self):
404 def _ip_default(self):
405 if self.transport == 'ipc':
405 if self.transport == 'ipc':
406 if self.connection_file:
406 if self.connection_file:
407 return os.path.splitext(self.connection_file)[0] + '-ipc'
407 return os.path.splitext(self.connection_file)[0] + '-ipc'
408 else:
408 else:
409 return 'kernel-ipc'
409 return 'kernel-ipc'
410 else:
410 else:
411 return localhost()
411 return localhost()
412
412
413 def _ip_changed(self, name, old, new):
413 def _ip_changed(self, name, old, new):
414 if new == '*':
414 if new == '*':
415 self.ip = '0.0.0.0'
415 self.ip = '0.0.0.0'
416
416
417 # protected traits
417 # protected traits
418
418
419 shell_port = Integer(0)
419 shell_port = Integer(0)
420 iopub_port = Integer(0)
420 iopub_port = Integer(0)
421 stdin_port = Integer(0)
421 stdin_port = Integer(0)
422 control_port = Integer(0)
422 control_port = Integer(0)
423 hb_port = Integer(0)
423 hb_port = Integer(0)
424
424
425 @property
425 @property
426 def ports(self):
426 def ports(self):
427 return [ getattr(self, name) for name in port_names ]
427 return [ getattr(self, name) for name in port_names ]
428
428
429 #--------------------------------------------------------------------------
429 #--------------------------------------------------------------------------
430 # Connection and ipc file management
430 # Connection and ipc file management
431 #--------------------------------------------------------------------------
431 #--------------------------------------------------------------------------
432
432
433 def get_connection_info(self):
433 def get_connection_info(self):
434 """return the connection info as a dict"""
434 """return the connection info as a dict"""
435 return dict(
435 return dict(
436 transport=self.transport,
436 transport=self.transport,
437 ip=self.ip,
437 ip=self.ip,
438 shell_port=self.shell_port,
438 shell_port=self.shell_port,
439 iopub_port=self.iopub_port,
439 iopub_port=self.iopub_port,
440 stdin_port=self.stdin_port,
440 stdin_port=self.stdin_port,
441 hb_port=self.hb_port,
441 hb_port=self.hb_port,
442 control_port=self.control_port,
442 control_port=self.control_port,
443 signature_scheme=self.session.signature_scheme,
443 signature_scheme=self.session.signature_scheme,
444 key=self.session.key,
444 key=self.session.key,
445 )
445 )
446
446
447 def cleanup_connection_file(self):
447 def cleanup_connection_file(self):
448 """Cleanup connection file *if we wrote it*
448 """Cleanup connection file *if we wrote it*
449
449
450 Will not raise if the connection file was already removed somehow.
450 Will not raise if the connection file was already removed somehow.
451 """
451 """
452 if self._connection_file_written:
452 if self._connection_file_written:
453 # cleanup connection files on full shutdown of kernel we started
453 # cleanup connection files on full shutdown of kernel we started
454 self._connection_file_written = False
454 self._connection_file_written = False
455 try:
455 try:
456 os.remove(self.connection_file)
456 os.remove(self.connection_file)
457 except (IOError, OSError, AttributeError):
457 except (IOError, OSError, AttributeError):
458 pass
458 pass
459
459
460 def cleanup_ipc_files(self):
460 def cleanup_ipc_files(self):
461 """Cleanup ipc files if we wrote them."""
461 """Cleanup ipc files if we wrote them."""
462 if self.transport != 'ipc':
462 if self.transport != 'ipc':
463 return
463 return
464 for port in self.ports:
464 for port in self.ports:
465 ipcfile = "%s-%i" % (self.ip, port)
465 ipcfile = "%s-%i" % (self.ip, port)
466 try:
466 try:
467 os.remove(ipcfile)
467 os.remove(ipcfile)
468 except (IOError, OSError):
468 except (IOError, OSError):
469 pass
469 pass
470
470
471 def write_connection_file(self):
471 def write_connection_file(self):
472 """Write connection info to JSON dict in self.connection_file."""
472 """Write connection info to JSON dict in self.connection_file."""
473 if self._connection_file_written:
473 if self._connection_file_written:
474 return
474 return
475
475
476 self.connection_file, cfg = write_connection_file(self.connection_file,
476 self.connection_file, cfg = write_connection_file(self.connection_file,
477 transport=self.transport, ip=self.ip, key=self.session.key,
477 transport=self.transport, ip=self.ip, key=self.session.key,
478 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
478 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
479 shell_port=self.shell_port, hb_port=self.hb_port,
479 shell_port=self.shell_port, hb_port=self.hb_port,
480 control_port=self.control_port,
480 control_port=self.control_port,
481 signature_scheme=self.session.signature_scheme,
481 signature_scheme=self.session.signature_scheme,
482 )
482 )
483 # write_connection_file also sets default ports:
483 # write_connection_file also sets default ports:
484 for name in port_names:
484 for name in port_names:
485 setattr(self, name, cfg[name])
485 setattr(self, name, cfg[name])
486
486
487 self._connection_file_written = True
487 self._connection_file_written = True
488
488
489 def load_connection_file(self):
489 def load_connection_file(self):
490 """Load connection info from JSON dict in self.connection_file."""
490 """Load connection info from JSON dict in self.connection_file."""
491 with open(self.connection_file) as f:
491 with open(self.connection_file) as f:
492 cfg = json.loads(f.read())
492 cfg = json.loads(f.read())
493
493
494 self.transport = cfg.get('transport', 'tcp')
494 self.transport = cfg.get('transport', 'tcp')
495 self.ip = cfg['ip']
495 self.ip = cfg['ip']
496 for name in port_names:
496 for name in port_names:
497 setattr(self, name, cfg[name])
497 setattr(self, name, cfg[name])
498 if 'key' in cfg:
498 if 'key' in cfg:
499 self.session.key = str_to_bytes(cfg['key'])
499 self.session.key = str_to_bytes(cfg['key'])
500 if cfg.get('signature_scheme'):
500 if cfg.get('signature_scheme'):
501 self.session.signature_scheme = cfg['signature_scheme']
501 self.session.signature_scheme = cfg['signature_scheme']
502
502
503 #--------------------------------------------------------------------------
503 #--------------------------------------------------------------------------
504 # Creating connected sockets
504 # Creating connected sockets
505 #--------------------------------------------------------------------------
505 #--------------------------------------------------------------------------
506
506
507 def _make_url(self, channel):
507 def _make_url(self, channel):
508 """Make a ZeroMQ URL for a given channel."""
508 """Make a ZeroMQ URL for a given channel."""
509 transport = self.transport
509 transport = self.transport
510 ip = self.ip
510 ip = self.ip
511 port = getattr(self, '%s_port' % channel)
511 port = getattr(self, '%s_port' % channel)
512
512
513 if transport == 'tcp':
513 if transport == 'tcp':
514 return "tcp://%s:%i" % (ip, port)
514 return "tcp://%s:%i" % (ip, port)
515 else:
515 else:
516 return "%s://%s-%s" % (transport, ip, port)
516 return "%s://%s-%s" % (transport, ip, port)
517
517
518 def _create_connected_socket(self, channel, identity=None):
518 def _create_connected_socket(self, channel, identity=None):
519 """Create a zmq Socket and connect it to the kernel."""
519 """Create a zmq Socket and connect it to the kernel."""
520 url = self._make_url(channel)
520 url = self._make_url(channel)
521 socket_type = channel_socket_types[channel]
521 socket_type = channel_socket_types[channel]
522 self.log.info("Connecting to: %s" % url)
522 self.log.debug("Connecting to: %s" % url)
523 sock = self.context.socket(socket_type)
523 sock = self.context.socket(socket_type)
524 if identity:
524 if identity:
525 sock.identity = identity
525 sock.identity = identity
526 sock.connect(url)
526 sock.connect(url)
527 return sock
527 return sock
528
528
529 def connect_iopub(self, identity=None):
529 def connect_iopub(self, identity=None):
530 """return zmq Socket connected to the IOPub channel"""
530 """return zmq Socket connected to the IOPub channel"""
531 sock = self._create_connected_socket('iopub', identity=identity)
531 sock = self._create_connected_socket('iopub', identity=identity)
532 sock.setsockopt(zmq.SUBSCRIBE, b'')
532 sock.setsockopt(zmq.SUBSCRIBE, b'')
533 return sock
533 return sock
534
534
535 def connect_shell(self, identity=None):
535 def connect_shell(self, identity=None):
536 """return zmq Socket connected to the Shell channel"""
536 """return zmq Socket connected to the Shell channel"""
537 return self._create_connected_socket('shell', identity=identity)
537 return self._create_connected_socket('shell', identity=identity)
538
538
539 def connect_stdin(self, identity=None):
539 def connect_stdin(self, identity=None):
540 """return zmq Socket connected to the StdIn channel"""
540 """return zmq Socket connected to the StdIn channel"""
541 return self._create_connected_socket('stdin', identity=identity)
541 return self._create_connected_socket('stdin', identity=identity)
542
542
543 def connect_hb(self, identity=None):
543 def connect_hb(self, identity=None):
544 """return zmq Socket connected to the Heartbeat channel"""
544 """return zmq Socket connected to the Heartbeat channel"""
545 return self._create_connected_socket('hb', identity=identity)
545 return self._create_connected_socket('hb', identity=identity)
546
546
547 def connect_control(self, identity=None):
547 def connect_control(self, identity=None):
548 """return zmq Socket connected to the Heartbeat channel"""
548 """return zmq Socket connected to the Heartbeat channel"""
549 return self._create_connected_socket('control', identity=identity)
549 return self._create_connected_socket('control', identity=identity)
550
550
551
551
552 __all__ = [
552 __all__ = [
553 'write_connection_file',
553 'write_connection_file',
554 'get_connection_file',
554 'get_connection_file',
555 'find_connection_file',
555 'find_connection_file',
556 'get_connection_info',
556 'get_connection_info',
557 'connect_qtconsole',
557 'connect_qtconsole',
558 'tunnel_to_kernel',
558 'tunnel_to_kernel',
559 ]
559 ]
General Comments 0
You need to be logged in to leave comments. Login now