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