##// END OF EJS Templates
use IOLoop.current in a few places...
MinRK -
Show More
@@ -1,257 +1,260 b''
1 1 # coding: utf-8
2 2 """Tornado handlers for WebSocket <-> ZMQ sockets."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import os
8 8 import json
9 9 import struct
10 10 import warnings
11 11
12 12 try:
13 13 from urllib.parse import urlparse # Py 3
14 14 except ImportError:
15 15 from urlparse import urlparse # Py 2
16 16
17 17 import tornado
18 18 from tornado import gen, ioloop, web
19 19 from tornado.websocket import WebSocketHandler
20 20
21 21 from IPython.kernel.zmq.session import Session
22 22 from IPython.utils.jsonutil import date_default, extract_dates
23 23 from IPython.utils.py3compat import cast_unicode
24 24
25 25 from .handlers import IPythonHandler
26 26
27 27 def serialize_binary_message(msg):
28 28 """serialize a message as a binary blob
29 29
30 30 Header:
31 31
32 32 4 bytes: number of msg parts (nbufs) as 32b int
33 33 4 * nbufs bytes: offset for each buffer as integer as 32b int
34 34
35 35 Offsets are from the start of the buffer, including the header.
36 36
37 37 Returns
38 38 -------
39 39
40 40 The message serialized to bytes.
41 41
42 42 """
43 43 # don't modify msg or buffer list in-place
44 44 msg = msg.copy()
45 45 buffers = list(msg.pop('buffers'))
46 46 bmsg = json.dumps(msg, default=date_default).encode('utf8')
47 47 buffers.insert(0, bmsg)
48 48 nbufs = len(buffers)
49 49 offsets = [4 * (nbufs + 1)]
50 50 for buf in buffers[:-1]:
51 51 offsets.append(offsets[-1] + len(buf))
52 52 offsets_buf = struct.pack('!' + 'I' * (nbufs + 1), nbufs, *offsets)
53 53 buffers.insert(0, offsets_buf)
54 54 return b''.join(buffers)
55 55
56 56
57 57 def deserialize_binary_message(bmsg):
58 58 """deserialize a message from a binary blog
59 59
60 60 Header:
61 61
62 62 4 bytes: number of msg parts (nbufs) as 32b int
63 63 4 * nbufs bytes: offset for each buffer as integer as 32b int
64 64
65 65 Offsets are from the start of the buffer, including the header.
66 66
67 67 Returns
68 68 -------
69 69
70 70 message dictionary
71 71 """
72 72 nbufs = struct.unpack('!i', bmsg[:4])[0]
73 73 offsets = list(struct.unpack('!' + 'I' * nbufs, bmsg[4:4*(nbufs+1)]))
74 74 offsets.append(None)
75 75 bufs = []
76 76 for start, stop in zip(offsets[:-1], offsets[1:]):
77 77 bufs.append(bmsg[start:stop])
78 78 msg = json.loads(bufs[0].decode('utf8'))
79 79 msg['header'] = extract_dates(msg['header'])
80 80 msg['parent_header'] = extract_dates(msg['parent_header'])
81 81 msg['buffers'] = bufs[1:]
82 82 return msg
83 83
84 84 # ping interval for keeping websockets alive (30 seconds)
85 85 WS_PING_INTERVAL = 30000
86 86
87 87 if os.environ.get('IPYTHON_ALLOW_DRAFT_WEBSOCKETS_FOR_PHANTOMJS', False):
88 88 warnings.warn("""Allowing draft76 websocket connections!
89 89 This should only be done for testing with phantomjs!""")
90 90 from IPython.html import allow76
91 91 WebSocketHandler = allow76.AllowDraftWebSocketHandler
92 92 # draft 76 doesn't support ping
93 93 WS_PING_INTERVAL = 0
94 94
95 95 class ZMQStreamHandler(WebSocketHandler):
96 96
97 97 def check_origin(self, origin):
98 98 """Check Origin == Host or Access-Control-Allow-Origin.
99 99
100 100 Tornado >= 4 calls this method automatically, raising 403 if it returns False.
101 101 We call it explicitly in `open` on Tornado < 4.
102 102 """
103 103 if self.allow_origin == '*':
104 104 return True
105 105
106 106 host = self.request.headers.get("Host")
107 107
108 108 # If no header is provided, assume we can't verify origin
109 109 if origin is None:
110 110 self.log.warn("Missing Origin header, rejecting WebSocket connection.")
111 111 return False
112 112 if host is None:
113 113 self.log.warn("Missing Host header, rejecting WebSocket connection.")
114 114 return False
115 115
116 116 origin = origin.lower()
117 117 origin_host = urlparse(origin).netloc
118 118
119 119 # OK if origin matches host
120 120 if origin_host == host:
121 121 return True
122 122
123 123 # Check CORS headers
124 124 if self.allow_origin:
125 125 allow = self.allow_origin == origin
126 126 elif self.allow_origin_pat:
127 127 allow = bool(self.allow_origin_pat.match(origin))
128 128 else:
129 129 # No CORS headers deny the request
130 130 allow = False
131 131 if not allow:
132 132 self.log.warn("Blocking Cross Origin WebSocket Attempt. Origin: %s, Host: %s",
133 133 origin, host,
134 134 )
135 135 return allow
136 136
137 137 def clear_cookie(self, *args, **kwargs):
138 138 """meaningless for websockets"""
139 139 pass
140 140
141 141 def _reserialize_reply(self, msg_list):
142 142 """Reserialize a reply message using JSON.
143 143
144 144 This takes the msg list from the ZMQ socket, deserializes it using
145 145 self.session and then serializes the result using JSON. This method
146 146 should be used by self._on_zmq_reply to build messages that can
147 147 be sent back to the browser.
148 148 """
149 149 idents, msg_list = self.session.feed_identities(msg_list)
150 150 msg = self.session.deserialize(msg_list)
151 151 if msg['buffers']:
152 152 buf = serialize_binary_message(msg)
153 153 return buf
154 154 else:
155 155 smsg = json.dumps(msg, default=date_default)
156 156 return cast_unicode(smsg)
157 157
158 158 def _on_zmq_reply(self, msg_list):
159 159 # Sometimes this gets triggered when the on_close method is scheduled in the
160 160 # eventloop but hasn't been called.
161 161 if self.stream.closed(): return
162 162 try:
163 163 msg = self._reserialize_reply(msg_list)
164 164 except Exception:
165 165 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
166 166 else:
167 167 self.write_message(msg, binary=isinstance(msg, bytes))
168 168
169 169 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
170 170 ping_callback = None
171 171 last_ping = 0
172 172 last_pong = 0
173 173
174 174 @property
175 175 def ping_interval(self):
176 176 """The interval for websocket keep-alive pings.
177 177
178 178 Set ws_ping_interval = 0 to disable pings.
179 179 """
180 180 return self.settings.get('ws_ping_interval', WS_PING_INTERVAL)
181 181
182 182 @property
183 183 def ping_timeout(self):
184 184 """If no ping is received in this many milliseconds,
185 185 close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
186 186 Default is max of 3 pings or 30 seconds.
187 187 """
188 188 return self.settings.get('ws_ping_timeout',
189 189 max(3 * self.ping_interval, WS_PING_INTERVAL)
190 190 )
191 191
192 192 def set_default_headers(self):
193 193 """Undo the set_default_headers in IPythonHandler
194 194
195 195 which doesn't make sense for websockets
196 196 """
197 197 pass
198 198
199 199 def pre_get(self):
200 200 """Run before finishing the GET request
201 201
202 202 Extend this method to add logic that should fire before
203 203 the websocket finishes completing.
204 204 """
205 205 # authenticate the request before opening the websocket
206 206 if self.get_current_user() is None:
207 207 self.log.warn("Couldn't authenticate WebSocket connection")
208 208 raise web.HTTPError(403)
209 209
210 210 if self.get_argument('session_id', False):
211 211 self.session.session = cast_unicode(self.get_argument('session_id'))
212 212 else:
213 213 self.log.warn("No session ID specified")
214 214
215 215 @gen.coroutine
216 216 def get(self, *args, **kwargs):
217 217 # pre_get can be a coroutine in subclasses
218 218 # assign and yield in two step to avoid tornado 3 issues
219 219 res = self.pre_get()
220 220 yield gen.maybe_future(res)
221 221 super(AuthenticatedZMQStreamHandler, self).get(*args, **kwargs)
222 222
223 223 def initialize(self):
224 224 self.log.debug("Initializing websocket connection %s", self.request.path)
225 225 self.session = Session(config=self.config)
226 226
227 227 def open(self, *args, **kwargs):
228 228 self.log.debug("Opening websocket %s", self.request.path)
229 229
230 230 # start the pinging
231 231 if self.ping_interval > 0:
232 self.last_ping = ioloop.IOLoop.instance().time() # Remember time of last ping
232 loop = ioloop.IOLoop.current()
233 self.last_ping = loop.time() # Remember time of last ping
233 234 self.last_pong = self.last_ping
234 self.ping_callback = ioloop.PeriodicCallback(self.send_ping, self.ping_interval)
235 self.ping_callback = ioloop.PeriodicCallback(
236 self.send_ping, self.ping_interval, io_loop=loop,
237 )
235 238 self.ping_callback.start()
236 239
237 240 def send_ping(self):
238 241 """send a ping to keep the websocket alive"""
239 242 if self.stream.closed() and self.ping_callback is not None:
240 243 self.ping_callback.stop()
241 244 return
242 245
243 246 # check for timeout on pong. Make sure that we really have sent a recent ping in
244 247 # case the machine with both server and client has been suspended since the last ping.
245 now = ioloop.IOLoop.instance().time()
248 now = ioloop.IOLoop.current().time()
246 249 since_last_pong = 1e3 * (now - self.last_pong)
247 250 since_last_ping = 1e3 * (now - self.last_ping)
248 251 if since_last_ping < 2*self.ping_interval and since_last_pong > self.ping_timeout:
249 252 self.log.warn("WebSocket ping timeout after %i ms.", since_last_pong)
250 253 self.close()
251 254 return
252 255
253 256 self.ping(b'')
254 257 self.last_ping = now
255 258
256 259 def on_pong(self, data):
257 self.last_pong = ioloop.IOLoop.instance().time()
260 self.last_pong = ioloop.IOLoop.current().time()
@@ -1,1042 +1,1050 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import datetime
11 11 import errno
12 12 import io
13 13 import json
14 14 import logging
15 15 import os
16 16 import random
17 17 import re
18 18 import select
19 19 import signal
20 20 import socket
21 21 import sys
22 22 import threading
23 23 import time
24 24 import webbrowser
25 25
26 26
27 27 # check for pyzmq 2.1.11
28 28 from IPython.utils.zmqrelated import check_for_zmq
29 29 check_for_zmq('2.1.11', 'IPython.html')
30 30
31 31 from jinja2 import Environment, FileSystemLoader
32 32
33 33 # Install the pyzmq ioloop. This has to be done before anything else from
34 34 # tornado is imported.
35 35 from zmq.eventloop import ioloop
36 36 ioloop.install()
37 37
38 38 # check for tornado 3.1.0
39 39 msg = "The IPython Notebook requires tornado >= 4.0"
40 40 try:
41 41 import tornado
42 42 except ImportError:
43 43 raise ImportError(msg)
44 44 try:
45 45 version_info = tornado.version_info
46 46 except AttributeError:
47 47 raise ImportError(msg + ", but you have < 1.1.0")
48 48 if version_info < (4,0):
49 49 raise ImportError(msg + ", but you have %s" % tornado.version)
50 50
51 51 from tornado import httpserver
52 52 from tornado import web
53 53 from tornado.log import LogFormatter, app_log, access_log, gen_log
54 54
55 55 from IPython.html import (
56 56 DEFAULT_STATIC_FILES_PATH,
57 57 DEFAULT_TEMPLATE_PATH_LIST,
58 58 )
59 59 from .base.handlers import Template404
60 60 from .log import log_request
61 61 from .services.kernels.kernelmanager import MappingKernelManager
62 62 from .services.contents.manager import ContentsManager
63 63 from .services.contents.filemanager import FileContentsManager
64 64 from .services.clusters.clustermanager import ClusterManager
65 65 from .services.sessions.sessionmanager import SessionManager
66 66
67 67 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
68 68
69 69 from IPython.config import Config
70 70 from IPython.config.application import catch_config_error, boolean_flag
71 71 from IPython.core.application import (
72 72 BaseIPythonApplication, base_flags, base_aliases,
73 73 )
74 74 from IPython.core.profiledir import ProfileDir
75 75 from IPython.kernel import KernelManager
76 76 from IPython.kernel.kernelspec import KernelSpecManager
77 77 from IPython.kernel.zmq.session import default_secure, Session
78 78 from IPython.nbformat.sign import NotebookNotary
79 79 from IPython.utils.importstring import import_item
80 80 from IPython.utils import submodule
81 81 from IPython.utils.process import check_pid
82 82 from IPython.utils.traitlets import (
83 83 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
84 84 DottedObjectName, TraitError,
85 85 )
86 86 from IPython.utils import py3compat
87 87 from IPython.utils.path import filefind, get_ipython_dir
88 88 from IPython.utils.sysinfo import get_sys_info
89 89
90 90 from .utils import url_path_join
91 91
92 92 #-----------------------------------------------------------------------------
93 93 # Module globals
94 94 #-----------------------------------------------------------------------------
95 95
96 96 _examples = """
97 97 ipython notebook # start the notebook
98 98 ipython notebook --profile=sympy # use the sympy profile
99 99 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
100 100 """
101 101
102 102 #-----------------------------------------------------------------------------
103 103 # Helper functions
104 104 #-----------------------------------------------------------------------------
105 105
106 106 def random_ports(port, n):
107 107 """Generate a list of n random ports near the given port.
108 108
109 109 The first 5 ports will be sequential, and the remaining n-5 will be
110 110 randomly selected in the range [port-2*n, port+2*n].
111 111 """
112 112 for i in range(min(5, n)):
113 113 yield port + i
114 114 for i in range(n-5):
115 115 yield max(1, port + random.randint(-2*n, 2*n))
116 116
117 117 def load_handlers(name):
118 118 """Load the (URL pattern, handler) tuples for each component."""
119 119 name = 'IPython.html.' + name
120 120 mod = __import__(name, fromlist=['default_handlers'])
121 121 return mod.default_handlers
122 122
123 123 #-----------------------------------------------------------------------------
124 124 # The Tornado web application
125 125 #-----------------------------------------------------------------------------
126 126
127 127 class NotebookWebApplication(web.Application):
128 128
129 129 def __init__(self, ipython_app, kernel_manager, contents_manager,
130 130 cluster_manager, session_manager, kernel_spec_manager,
131 131 config_manager, log,
132 132 base_url, default_url, settings_overrides, jinja_env_options):
133 133
134 134 settings = self.init_settings(
135 135 ipython_app, kernel_manager, contents_manager, cluster_manager,
136 136 session_manager, kernel_spec_manager, config_manager, log, base_url,
137 137 default_url, settings_overrides, jinja_env_options)
138 138 handlers = self.init_handlers(settings)
139 139
140 140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 141
142 142 def init_settings(self, ipython_app, kernel_manager, contents_manager,
143 143 cluster_manager, session_manager, kernel_spec_manager,
144 144 config_manager,
145 145 log, base_url, default_url, settings_overrides,
146 146 jinja_env_options=None):
147 147
148 148 _template_path = settings_overrides.get(
149 149 "template_path",
150 150 ipython_app.template_file_path,
151 151 )
152 152 if isinstance(_template_path, str):
153 153 _template_path = (_template_path,)
154 154 template_path = [os.path.expanduser(path) for path in _template_path]
155 155
156 156 jenv_opt = jinja_env_options if jinja_env_options else {}
157 157 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
158 158
159 159 sys_info = get_sys_info()
160 160 if sys_info['commit_source'] == 'repository':
161 161 # don't cache (rely on 304) when working from master
162 162 version_hash = ''
163 163 else:
164 164 # reset the cache on server restart
165 165 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
166 166
167 167 settings = dict(
168 168 # basics
169 169 log_function=log_request,
170 170 base_url=base_url,
171 171 default_url=default_url,
172 172 template_path=template_path,
173 173 static_path=ipython_app.static_file_path,
174 174 static_handler_class = FileFindHandler,
175 175 static_url_prefix = url_path_join(base_url,'/static/'),
176 176 static_handler_args = {
177 177 # don't cache custom.js
178 178 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
179 179 },
180 180 version_hash=version_hash,
181 181
182 182 # authentication
183 183 cookie_secret=ipython_app.cookie_secret,
184 184 login_url=url_path_join(base_url,'/login'),
185 185 password=ipython_app.password,
186 186
187 187 # managers
188 188 kernel_manager=kernel_manager,
189 189 contents_manager=contents_manager,
190 190 cluster_manager=cluster_manager,
191 191 session_manager=session_manager,
192 192 kernel_spec_manager=kernel_spec_manager,
193 193 config_manager=config_manager,
194 194
195 195 # IPython stuff
196 196 nbextensions_path = ipython_app.nbextensions_path,
197 197 websocket_url=ipython_app.websocket_url,
198 198 mathjax_url=ipython_app.mathjax_url,
199 199 config=ipython_app.config,
200 200 jinja2_env=env,
201 201 terminals_available=False, # Set later if terminals are available
202 202 )
203 203
204 204 # allow custom overrides for the tornado web app.
205 205 settings.update(settings_overrides)
206 206 return settings
207 207
208 208 def init_handlers(self, settings):
209 209 """Load the (URL pattern, handler) tuples for each component."""
210 210
211 211 # Order matters. The first handler to match the URL will handle the request.
212 212 handlers = []
213 213 handlers.extend(load_handlers('tree.handlers'))
214 214 handlers.extend(load_handlers('auth.login'))
215 215 handlers.extend(load_handlers('auth.logout'))
216 216 handlers.extend(load_handlers('files.handlers'))
217 217 handlers.extend(load_handlers('notebook.handlers'))
218 218 handlers.extend(load_handlers('nbconvert.handlers'))
219 219 handlers.extend(load_handlers('kernelspecs.handlers'))
220 220 handlers.extend(load_handlers('edit.handlers'))
221 221 handlers.extend(load_handlers('services.config.handlers'))
222 222 handlers.extend(load_handlers('services.kernels.handlers'))
223 223 handlers.extend(load_handlers('services.contents.handlers'))
224 224 handlers.extend(load_handlers('services.clusters.handlers'))
225 225 handlers.extend(load_handlers('services.sessions.handlers'))
226 226 handlers.extend(load_handlers('services.nbconvert.handlers'))
227 227 handlers.extend(load_handlers('services.kernelspecs.handlers'))
228 228 handlers.extend(load_handlers('services.security.handlers'))
229 229 handlers.append(
230 230 (r"/nbextensions/(.*)", FileFindHandler, {
231 231 'path': settings['nbextensions_path'],
232 232 'no_cache_paths': ['/'], # don't cache anything in nbextensions
233 233 }),
234 234 )
235 235 # register base handlers last
236 236 handlers.extend(load_handlers('base.handlers'))
237 237 # set the URL that will be redirected from `/`
238 238 handlers.append(
239 239 (r'/?', web.RedirectHandler, {
240 240 'url' : url_path_join(settings['base_url'], settings['default_url']),
241 241 'permanent': False, # want 302, not 301
242 242 })
243 243 )
244 244 # prepend base_url onto the patterns that we match
245 245 new_handlers = []
246 246 for handler in handlers:
247 247 pattern = url_path_join(settings['base_url'], handler[0])
248 248 new_handler = tuple([pattern] + list(handler[1:]))
249 249 new_handlers.append(new_handler)
250 250 # add 404 on the end, which will catch everything that falls through
251 251 new_handlers.append((r'(.*)', Template404))
252 252 return new_handlers
253 253
254 254
255 255 class NbserverListApp(BaseIPythonApplication):
256 256
257 257 description="List currently running notebook servers in this profile."
258 258
259 259 flags = dict(
260 260 json=({'NbserverListApp': {'json': True}},
261 261 "Produce machine-readable JSON output."),
262 262 )
263 263
264 264 json = Bool(False, config=True,
265 265 help="If True, each line of output will be a JSON object with the "
266 266 "details from the server info file.")
267 267
268 268 def start(self):
269 269 if not self.json:
270 270 print("Currently running servers:")
271 271 for serverinfo in list_running_servers(self.profile):
272 272 if self.json:
273 273 print(json.dumps(serverinfo))
274 274 else:
275 275 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
276 276
277 277 #-----------------------------------------------------------------------------
278 278 # Aliases and Flags
279 279 #-----------------------------------------------------------------------------
280 280
281 281 flags = dict(base_flags)
282 282 flags['no-browser']=(
283 283 {'NotebookApp' : {'open_browser' : False}},
284 284 "Don't open the notebook in a browser after startup."
285 285 )
286 286 flags['pylab']=(
287 287 {'NotebookApp' : {'pylab' : 'warn'}},
288 288 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
289 289 )
290 290 flags['no-mathjax']=(
291 291 {'NotebookApp' : {'enable_mathjax' : False}},
292 292 """Disable MathJax
293 293
294 294 MathJax is the javascript library IPython uses to render math/LaTeX. It is
295 295 very large, so you may want to disable it if you have a slow internet
296 296 connection, or for offline use of the notebook.
297 297
298 298 When disabled, equations etc. will appear as their untransformed TeX source.
299 299 """
300 300 )
301 301
302 302 # Add notebook manager flags
303 303 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
304 304 'DEPRECATED, IGNORED',
305 305 'DEPRECATED, IGNORED'))
306 306
307 307 aliases = dict(base_aliases)
308 308
309 309 aliases.update({
310 310 'ip': 'NotebookApp.ip',
311 311 'port': 'NotebookApp.port',
312 312 'port-retries': 'NotebookApp.port_retries',
313 313 'transport': 'KernelManager.transport',
314 314 'keyfile': 'NotebookApp.keyfile',
315 315 'certfile': 'NotebookApp.certfile',
316 316 'notebook-dir': 'NotebookApp.notebook_dir',
317 317 'browser': 'NotebookApp.browser',
318 318 'pylab': 'NotebookApp.pylab',
319 319 })
320 320
321 321 #-----------------------------------------------------------------------------
322 322 # NotebookApp
323 323 #-----------------------------------------------------------------------------
324 324
325 325 class NotebookApp(BaseIPythonApplication):
326 326
327 327 name = 'ipython-notebook'
328 328
329 329 description = """
330 330 The IPython HTML Notebook.
331 331
332 332 This launches a Tornado based HTML Notebook Server that serves up an
333 333 HTML5/Javascript Notebook client.
334 334 """
335 335 examples = _examples
336 336 aliases = aliases
337 337 flags = flags
338 338
339 339 classes = [
340 340 KernelManager, ProfileDir, Session, MappingKernelManager,
341 341 ContentsManager, FileContentsManager, NotebookNotary,
342 342 ]
343 343 flags = Dict(flags)
344 344 aliases = Dict(aliases)
345 345
346 346 subcommands = dict(
347 347 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
348 348 )
349 349
350 350 ipython_kernel_argv = List(Unicode)
351 351
352 352 _log_formatter_cls = LogFormatter
353 353
354 354 def _log_level_default(self):
355 355 return logging.INFO
356 356
357 357 def _log_datefmt_default(self):
358 358 """Exclude date from default date format"""
359 359 return "%H:%M:%S"
360 360
361 361 def _log_format_default(self):
362 362 """override default log format to include time"""
363 363 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
364 364
365 365 # create requested profiles by default, if they don't exist:
366 366 auto_create = Bool(True)
367 367
368 368 # file to be opened in the notebook server
369 369 file_to_run = Unicode('', config=True)
370 370
371 371 # Network related information
372 372
373 373 allow_origin = Unicode('', config=True,
374 374 help="""Set the Access-Control-Allow-Origin header
375 375
376 376 Use '*' to allow any origin to access your server.
377 377
378 378 Takes precedence over allow_origin_pat.
379 379 """
380 380 )
381 381
382 382 allow_origin_pat = Unicode('', config=True,
383 383 help="""Use a regular expression for the Access-Control-Allow-Origin header
384 384
385 385 Requests from an origin matching the expression will get replies with:
386 386
387 387 Access-Control-Allow-Origin: origin
388 388
389 389 where `origin` is the origin of the request.
390 390
391 391 Ignored if allow_origin is set.
392 392 """
393 393 )
394 394
395 395 allow_credentials = Bool(False, config=True,
396 396 help="Set the Access-Control-Allow-Credentials: true header"
397 397 )
398 398
399 399 default_url = Unicode('/tree', config=True,
400 400 help="The default URL to redirect to from `/`"
401 401 )
402 402
403 403 ip = Unicode('localhost', config=True,
404 404 help="The IP address the notebook server will listen on."
405 405 )
406 406
407 407 def _ip_changed(self, name, old, new):
408 408 if new == u'*': self.ip = u''
409 409
410 410 port = Integer(8888, config=True,
411 411 help="The port the notebook server will listen on."
412 412 )
413 413 port_retries = Integer(50, config=True,
414 414 help="The number of additional ports to try if the specified port is not available."
415 415 )
416 416
417 417 certfile = Unicode(u'', config=True,
418 418 help="""The full path to an SSL/TLS certificate file."""
419 419 )
420 420
421 421 keyfile = Unicode(u'', config=True,
422 422 help="""The full path to a private key file for usage with SSL/TLS."""
423 423 )
424 424
425 425 cookie_secret_file = Unicode(config=True,
426 426 help="""The file where the cookie secret is stored."""
427 427 )
428 428 def _cookie_secret_file_default(self):
429 429 if self.profile_dir is None:
430 430 return ''
431 431 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
432 432
433 433 cookie_secret = Bytes(b'', config=True,
434 434 help="""The random bytes used to secure cookies.
435 435 By default this is a new random number every time you start the Notebook.
436 436 Set it to a value in a config file to enable logins to persist across server sessions.
437 437
438 438 Note: Cookie secrets should be kept private, do not share config files with
439 439 cookie_secret stored in plaintext (you can read the value from a file).
440 440 """
441 441 )
442 442 def _cookie_secret_default(self):
443 443 if os.path.exists(self.cookie_secret_file):
444 444 with io.open(self.cookie_secret_file, 'rb') as f:
445 445 return f.read()
446 446 else:
447 447 secret = base64.encodestring(os.urandom(1024))
448 448 self._write_cookie_secret_file(secret)
449 449 return secret
450 450
451 451 def _write_cookie_secret_file(self, secret):
452 452 """write my secret to my secret_file"""
453 453 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
454 454 with io.open(self.cookie_secret_file, 'wb') as f:
455 455 f.write(secret)
456 456 try:
457 457 os.chmod(self.cookie_secret_file, 0o600)
458 458 except OSError:
459 459 self.log.warn(
460 460 "Could not set permissions on %s",
461 461 self.cookie_secret_file
462 462 )
463 463
464 464 password = Unicode(u'', config=True,
465 465 help="""Hashed password to use for web authentication.
466 466
467 467 To generate, type in a python/IPython shell:
468 468
469 469 from IPython.lib import passwd; passwd()
470 470
471 471 The string should be of the form type:salt:hashed-password.
472 472 """
473 473 )
474 474
475 475 open_browser = Bool(True, config=True,
476 476 help="""Whether to open in a browser after starting.
477 477 The specific browser used is platform dependent and
478 478 determined by the python standard library `webbrowser`
479 479 module, unless it is overridden using the --browser
480 480 (NotebookApp.browser) configuration option.
481 481 """)
482 482
483 483 browser = Unicode(u'', config=True,
484 484 help="""Specify what command to use to invoke a web
485 485 browser when opening the notebook. If not specified, the
486 486 default browser will be determined by the `webbrowser`
487 487 standard library module, which allows setting of the
488 488 BROWSER environment variable to override it.
489 489 """)
490 490
491 491 webapp_settings = Dict(config=True,
492 492 help="DEPRECATED, use tornado_settings"
493 493 )
494 494 def _webapp_settings_changed(self, name, old, new):
495 495 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
496 496 self.tornado_settings = new
497 497
498 498 tornado_settings = Dict(config=True,
499 499 help="Supply overrides for the tornado.web.Application that the "
500 500 "IPython notebook uses.")
501 501
502 502 jinja_environment_options = Dict(config=True,
503 503 help="Supply extra arguments that will be passed to Jinja environment.")
504 504
505 505
506 506 enable_mathjax = Bool(True, config=True,
507 507 help="""Whether to enable MathJax for typesetting math/TeX
508 508
509 509 MathJax is the javascript library IPython uses to render math/LaTeX. It is
510 510 very large, so you may want to disable it if you have a slow internet
511 511 connection, or for offline use of the notebook.
512 512
513 513 When disabled, equations etc. will appear as their untransformed TeX source.
514 514 """
515 515 )
516 516 def _enable_mathjax_changed(self, name, old, new):
517 517 """set mathjax url to empty if mathjax is disabled"""
518 518 if not new:
519 519 self.mathjax_url = u''
520 520
521 521 base_url = Unicode('/', config=True,
522 522 help='''The base URL for the notebook server.
523 523
524 524 Leading and trailing slashes can be omitted,
525 525 and will automatically be added.
526 526 ''')
527 527 def _base_url_changed(self, name, old, new):
528 528 if not new.startswith('/'):
529 529 self.base_url = '/'+new
530 530 elif not new.endswith('/'):
531 531 self.base_url = new+'/'
532 532
533 533 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
534 534 def _base_project_url_changed(self, name, old, new):
535 535 self.log.warn("base_project_url is deprecated, use base_url")
536 536 self.base_url = new
537 537
538 538 extra_static_paths = List(Unicode, config=True,
539 539 help="""Extra paths to search for serving static files.
540 540
541 541 This allows adding javascript/css to be available from the notebook server machine,
542 542 or overriding individual files in the IPython"""
543 543 )
544 544 def _extra_static_paths_default(self):
545 545 return [os.path.join(self.profile_dir.location, 'static')]
546 546
547 547 @property
548 548 def static_file_path(self):
549 549 """return extra paths + the default location"""
550 550 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
551 551
552 552 extra_template_paths = List(Unicode, config=True,
553 553 help="""Extra paths to search for serving jinja templates.
554 554
555 555 Can be used to override templates from IPython.html.templates."""
556 556 )
557 557 def _extra_template_paths_default(self):
558 558 return []
559 559
560 560 @property
561 561 def template_file_path(self):
562 562 """return extra paths + the default locations"""
563 563 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
564 564
565 565 nbextensions_path = List(Unicode, config=True,
566 566 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
567 567 )
568 568 def _nbextensions_path_default(self):
569 569 return [os.path.join(get_ipython_dir(), 'nbextensions')]
570 570
571 571 websocket_url = Unicode("", config=True,
572 572 help="""The base URL for websockets,
573 573 if it differs from the HTTP server (hint: it almost certainly doesn't).
574 574
575 575 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
576 576 """
577 577 )
578 578 mathjax_url = Unicode("", config=True,
579 579 help="""The url for MathJax.js."""
580 580 )
581 581 def _mathjax_url_default(self):
582 582 if not self.enable_mathjax:
583 583 return u''
584 584 static_url_prefix = self.tornado_settings.get("static_url_prefix",
585 585 url_path_join(self.base_url, "static")
586 586 )
587 587
588 588 # try local mathjax, either in nbextensions/mathjax or static/mathjax
589 589 for (url_prefix, search_path) in [
590 590 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
591 591 (static_url_prefix, self.static_file_path),
592 592 ]:
593 593 self.log.debug("searching for local mathjax in %s", search_path)
594 594 try:
595 595 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
596 596 except IOError:
597 597 continue
598 598 else:
599 599 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
600 600 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
601 601 return url
602 602
603 603 # no local mathjax, serve from CDN
604 604 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
605 605 self.log.info("Using MathJax from CDN: %s", url)
606 606 return url
607 607
608 608 def _mathjax_url_changed(self, name, old, new):
609 609 if new and not self.enable_mathjax:
610 610 # enable_mathjax=False overrides mathjax_url
611 611 self.mathjax_url = u''
612 612 else:
613 613 self.log.info("Using MathJax: %s", new)
614 614
615 615 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
616 616 config=True,
617 617 help='The notebook manager class to use.'
618 618 )
619 619 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
620 620 config=True,
621 621 help='The kernel manager class to use.'
622 622 )
623 623 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
624 624 config=True,
625 625 help='The session manager class to use.'
626 626 )
627 627 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
628 628 config=True,
629 629 help='The cluster manager class to use.'
630 630 )
631 631
632 632 config_manager_class = DottedObjectName('IPython.html.services.config.manager.ConfigManager',
633 633 config = True,
634 634 help='The config manager class to use'
635 635 )
636 636
637 637 kernel_spec_manager = Instance(KernelSpecManager)
638 638
639 639 def _kernel_spec_manager_default(self):
640 640 return KernelSpecManager(ipython_dir=self.ipython_dir)
641 641
642 642
643 643 kernel_spec_manager_class = DottedObjectName('IPython.kernel.kernelspec.KernelSpecManager',
644 644 config=True,
645 645 help="""
646 646 The kernel spec manager class to use. Should be a subclass
647 647 of `IPython.kernel.kernelspec.KernelSpecManager`.
648 648
649 649 The Api of KernelSpecManager is provisional and might change
650 650 without warning between this version of IPython and the next stable one.
651 651 """)
652 652
653 653 trust_xheaders = Bool(False, config=True,
654 654 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
655 655 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
656 656 )
657 657
658 658 info_file = Unicode()
659 659
660 660 def _info_file_default(self):
661 661 info_file = "nbserver-%s.json"%os.getpid()
662 662 return os.path.join(self.profile_dir.security_dir, info_file)
663 663
664 664 pylab = Unicode('disabled', config=True,
665 665 help="""
666 666 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
667 667 """
668 668 )
669 669 def _pylab_changed(self, name, old, new):
670 670 """when --pylab is specified, display a warning and exit"""
671 671 if new != 'warn':
672 672 backend = ' %s' % new
673 673 else:
674 674 backend = ''
675 675 self.log.error("Support for specifying --pylab on the command line has been removed.")
676 676 self.log.error(
677 677 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
678 678 )
679 679 self.exit(1)
680 680
681 681 notebook_dir = Unicode(config=True,
682 682 help="The directory to use for notebooks and kernels."
683 683 )
684 684
685 685 def _notebook_dir_default(self):
686 686 if self.file_to_run:
687 687 return os.path.dirname(os.path.abspath(self.file_to_run))
688 688 else:
689 689 return py3compat.getcwd()
690 690
691 691 def _notebook_dir_changed(self, name, old, new):
692 692 """Do a bit of validation of the notebook dir."""
693 693 if not os.path.isabs(new):
694 694 # If we receive a non-absolute path, make it absolute.
695 695 self.notebook_dir = os.path.abspath(new)
696 696 return
697 697 if not os.path.isdir(new):
698 698 raise TraitError("No such notebook dir: %r" % new)
699 699
700 700 # setting App.notebook_dir implies setting notebook and kernel dirs as well
701 701 self.config.FileContentsManager.root_dir = new
702 702 self.config.MappingKernelManager.root_dir = new
703 703
704 704
705 705 def parse_command_line(self, argv=None):
706 706 super(NotebookApp, self).parse_command_line(argv)
707 707
708 708 if self.extra_args:
709 709 arg0 = self.extra_args[0]
710 710 f = os.path.abspath(arg0)
711 711 self.argv.remove(arg0)
712 712 if not os.path.exists(f):
713 713 self.log.critical("No such file or directory: %s", f)
714 714 self.exit(1)
715 715
716 716 # Use config here, to ensure that it takes higher priority than
717 717 # anything that comes from the profile.
718 718 c = Config()
719 719 if os.path.isdir(f):
720 720 c.NotebookApp.notebook_dir = f
721 721 elif os.path.isfile(f):
722 722 c.NotebookApp.file_to_run = f
723 723 self.update_config(c)
724 724
725 725 def init_kernel_argv(self):
726 726 """add the profile-dir to arguments to be passed to IPython kernels"""
727 727 # FIXME: remove special treatment of IPython kernels
728 728 # Kernel should get *absolute* path to profile directory
729 729 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
730 730
731 731 def init_configurables(self):
732 732 # force Session default to be secure
733 733 default_secure(self.config)
734 734 kls = import_item(self.kernel_spec_manager_class)
735 735 self.kernel_spec_manager = kls(ipython_dir=self.ipython_dir)
736 736
737 737 kls = import_item(self.kernel_manager_class)
738 738 self.kernel_manager = kls(
739 739 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
740 740 connection_dir = self.profile_dir.security_dir,
741 741 )
742 742 kls = import_item(self.contents_manager_class)
743 743 self.contents_manager = kls(parent=self, log=self.log)
744 744 kls = import_item(self.session_manager_class)
745 745 self.session_manager = kls(parent=self, log=self.log,
746 746 kernel_manager=self.kernel_manager,
747 747 contents_manager=self.contents_manager)
748 748 kls = import_item(self.cluster_manager_class)
749 749 self.cluster_manager = kls(parent=self, log=self.log)
750 750 self.cluster_manager.update_profiles()
751 751
752 752 kls = import_item(self.config_manager_class)
753 753 self.config_manager = kls(parent=self, log=self.log,
754 754 profile_dir=self.profile_dir.location)
755 755
756 756 def init_logging(self):
757 757 # This prevents double log messages because tornado use a root logger that
758 758 # self.log is a child of. The logging module dipatches log messages to a log
759 759 # and all of its ancenstors until propagate is set to False.
760 760 self.log.propagate = False
761 761
762 762 for log in app_log, access_log, gen_log:
763 763 # consistent log output name (NotebookApp instead of tornado.access, etc.)
764 764 log.name = self.log.name
765 765 # hook up tornado 3's loggers to our app handlers
766 766 logger = logging.getLogger('tornado')
767 767 logger.propagate = True
768 768 logger.parent = self.log
769 769 logger.setLevel(self.log.level)
770 770
771 771 def init_webapp(self):
772 772 """initialize tornado webapp and httpserver"""
773 773 self.tornado_settings['allow_origin'] = self.allow_origin
774 774 if self.allow_origin_pat:
775 775 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
776 776 self.tornado_settings['allow_credentials'] = self.allow_credentials
777 777
778 778 self.web_app = NotebookWebApplication(
779 779 self, self.kernel_manager, self.contents_manager,
780 780 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
781 781 self.config_manager,
782 782 self.log, self.base_url, self.default_url, self.tornado_settings,
783 783 self.jinja_environment_options
784 784 )
785 785 if self.certfile:
786 786 ssl_options = dict(certfile=self.certfile)
787 787 if self.keyfile:
788 788 ssl_options['keyfile'] = self.keyfile
789 789 else:
790 790 ssl_options = None
791 791 self.web_app.password = self.password
792 792 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
793 793 xheaders=self.trust_xheaders)
794 794 if not self.ip:
795 795 warning = "WARNING: The notebook server is listening on all IP addresses"
796 796 if ssl_options is None:
797 797 self.log.critical(warning + " and not using encryption. This "
798 798 "is not recommended.")
799 799 if not self.password:
800 800 self.log.critical(warning + " and not using authentication. "
801 801 "This is highly insecure and not recommended.")
802 802 success = None
803 803 for port in random_ports(self.port, self.port_retries+1):
804 804 try:
805 805 self.http_server.listen(port, self.ip)
806 806 except socket.error as e:
807 807 if e.errno == errno.EADDRINUSE:
808 808 self.log.info('The port %i is already in use, trying another random port.' % port)
809 809 continue
810 810 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
811 811 self.log.warn("Permission to listen on port %i denied" % port)
812 812 continue
813 813 else:
814 814 raise
815 815 else:
816 816 self.port = port
817 817 success = True
818 818 break
819 819 if not success:
820 820 self.log.critical('ERROR: the notebook server could not be started because '
821 821 'no available port could be found.')
822 822 self.exit(1)
823 823
824 824 @property
825 825 def display_url(self):
826 826 ip = self.ip if self.ip else '[all ip addresses on your system]'
827 827 return self._url(ip)
828 828
829 829 @property
830 830 def connection_url(self):
831 831 ip = self.ip if self.ip else 'localhost'
832 832 return self._url(ip)
833 833
834 834 def _url(self, ip):
835 835 proto = 'https' if self.certfile else 'http'
836 836 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
837 837
838 838 def init_terminals(self):
839 839 try:
840 840 from .terminal import initialize
841 841 initialize(self.web_app)
842 842 self.web_app.settings['terminals_available'] = True
843 843 except ImportError as e:
844 844 self.log.info("Terminals not available (error was %s)", e)
845 845
846 846 def init_signal(self):
847 847 if not sys.platform.startswith('win'):
848 848 signal.signal(signal.SIGINT, self._handle_sigint)
849 849 signal.signal(signal.SIGTERM, self._signal_stop)
850 850 if hasattr(signal, 'SIGUSR1'):
851 851 # Windows doesn't support SIGUSR1
852 852 signal.signal(signal.SIGUSR1, self._signal_info)
853 853 if hasattr(signal, 'SIGINFO'):
854 854 # only on BSD-based systems
855 855 signal.signal(signal.SIGINFO, self._signal_info)
856 856
857 857 def _handle_sigint(self, sig, frame):
858 858 """SIGINT handler spawns confirmation dialog"""
859 859 # register more forceful signal handler for ^C^C case
860 860 signal.signal(signal.SIGINT, self._signal_stop)
861 861 # request confirmation dialog in bg thread, to avoid
862 862 # blocking the App
863 863 thread = threading.Thread(target=self._confirm_exit)
864 864 thread.daemon = True
865 865 thread.start()
866 866
867 867 def _restore_sigint_handler(self):
868 868 """callback for restoring original SIGINT handler"""
869 869 signal.signal(signal.SIGINT, self._handle_sigint)
870 870
871 871 def _confirm_exit(self):
872 872 """confirm shutdown on ^C
873 873
874 874 A second ^C, or answering 'y' within 5s will cause shutdown,
875 875 otherwise original SIGINT handler will be restored.
876 876
877 877 This doesn't work on Windows.
878 878 """
879 879 info = self.log.info
880 880 info('interrupted')
881 881 print(self.notebook_info())
882 882 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
883 883 sys.stdout.flush()
884 884 r,w,x = select.select([sys.stdin], [], [], 5)
885 885 if r:
886 886 line = sys.stdin.readline()
887 887 if line.lower().startswith('y') and 'n' not in line.lower():
888 888 self.log.critical("Shutdown confirmed")
889 ioloop.IOLoop.instance().stop()
889 ioloop.IOLoop.current().stop()
890 890 return
891 891 else:
892 892 print("No answer for 5s:", end=' ')
893 893 print("resuming operation...")
894 894 # no answer, or answer is no:
895 895 # set it back to original SIGINT handler
896 896 # use IOLoop.add_callback because signal.signal must be called
897 897 # from main thread
898 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
898 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
899 899
900 900 def _signal_stop(self, sig, frame):
901 901 self.log.critical("received signal %s, stopping", sig)
902 ioloop.IOLoop.instance().stop()
902 ioloop.IOLoop.current().stop()
903 903
904 904 def _signal_info(self, sig, frame):
905 905 print(self.notebook_info())
906 906
907 907 def init_components(self):
908 908 """Check the components submodule, and warn if it's unclean"""
909 909 status = submodule.check_submodule_status()
910 910 if status == 'missing':
911 911 self.log.warn("components submodule missing, running `git submodule update`")
912 912 submodule.update_submodules(submodule.ipython_parent())
913 913 elif status == 'unclean':
914 914 self.log.warn("components submodule unclean, you may see 404s on static/components")
915 915 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
916 916
917 917 @catch_config_error
918 918 def initialize(self, argv=None):
919 919 super(NotebookApp, self).initialize(argv)
920 920 self.init_logging()
921 921 self.init_kernel_argv()
922 922 self.init_configurables()
923 923 self.init_components()
924 924 self.init_webapp()
925 925 self.init_terminals()
926 926 self.init_signal()
927 927
928 928 def cleanup_kernels(self):
929 929 """Shutdown all kernels.
930 930
931 931 The kernels will shutdown themselves when this process no longer exists,
932 932 but explicit shutdown allows the KernelManagers to cleanup the connection files.
933 933 """
934 934 self.log.info('Shutting down kernels')
935 935 self.kernel_manager.shutdown_all()
936 936
937 937 def notebook_info(self):
938 938 "Return the current working directory and the server url information"
939 939 info = self.contents_manager.info_string() + "\n"
940 940 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
941 941 return info + "The IPython Notebook is running at: %s" % self.display_url
942 942
943 943 def server_info(self):
944 944 """Return a JSONable dict of information about this server."""
945 945 return {'url': self.connection_url,
946 946 'hostname': self.ip if self.ip else 'localhost',
947 947 'port': self.port,
948 948 'secure': bool(self.certfile),
949 949 'base_url': self.base_url,
950 950 'notebook_dir': os.path.abspath(self.notebook_dir),
951 951 'pid': os.getpid()
952 952 }
953 953
954 954 def write_server_info_file(self):
955 955 """Write the result of server_info() to the JSON file info_file."""
956 956 with open(self.info_file, 'w') as f:
957 957 json.dump(self.server_info(), f, indent=2)
958 958
959 959 def remove_server_info_file(self):
960 960 """Remove the nbserver-<pid>.json file created for this server.
961 961
962 962 Ignores the error raised when the file has already been removed.
963 963 """
964 964 try:
965 965 os.unlink(self.info_file)
966 966 except OSError as e:
967 967 if e.errno != errno.ENOENT:
968 968 raise
969 969
970 970 def start(self):
971 971 """ Start the IPython Notebook server app, after initialization
972 972
973 973 This method takes no arguments so all configuration and initialization
974 974 must be done prior to calling this method."""
975 975 if self.subapp is not None:
976 976 return self.subapp.start()
977 977
978 978 info = self.log.info
979 979 for line in self.notebook_info().split("\n"):
980 980 info(line)
981 981 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
982 982
983 983 self.write_server_info_file()
984 984
985 985 if self.open_browser or self.file_to_run:
986 986 try:
987 987 browser = webbrowser.get(self.browser or None)
988 988 except webbrowser.Error as e:
989 989 self.log.warn('No web browser found: %s.' % e)
990 990 browser = None
991 991
992 992 if self.file_to_run:
993 993 if not os.path.exists(self.file_to_run):
994 994 self.log.critical("%s does not exist" % self.file_to_run)
995 995 self.exit(1)
996 996
997 997 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
998 998 uri = url_path_join('notebooks', *relpath.split(os.sep))
999 999 else:
1000 1000 uri = 'tree'
1001 1001 if browser:
1002 1002 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1003 1003 new=2)
1004 1004 threading.Thread(target=b).start()
1005
1006 self.io_loop = ioloop.IOLoop.current()
1005 1007 try:
1006 ioloop.IOLoop.instance().start()
1008 self.io_loop.start()
1007 1009 except KeyboardInterrupt:
1008 1010 info("Interrupted...")
1009 1011 finally:
1010 1012 self.cleanup_kernels()
1011 1013 self.remove_server_info_file()
1014
1015 def stop(self):
1016 def _stop():
1017 self.http_server.stop()
1018 self.io_loop.stop()
1019 self.io_loop.add_callback(_stop)
1012 1020
1013 1021
1014 1022 def list_running_servers(profile='default'):
1015 1023 """Iterate over the server info files of running notebook servers.
1016 1024
1017 1025 Given a profile name, find nbserver-* files in the security directory of
1018 1026 that profile, and yield dicts of their information, each one pertaining to
1019 1027 a currently running notebook server instance.
1020 1028 """
1021 1029 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1022 1030 for file in os.listdir(pd.security_dir):
1023 1031 if file.startswith('nbserver-'):
1024 1032 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1025 1033 info = json.load(f)
1026 1034
1027 1035 # Simple check whether that process is really still running
1028 1036 # Also remove leftover files from IPython 2.x without a pid field
1029 1037 if ('pid' in info) and check_pid(info['pid']):
1030 1038 yield info
1031 1039 else:
1032 1040 # If the process has died, try to delete its info file
1033 1041 try:
1034 1042 os.unlink(file)
1035 1043 except OSError:
1036 1044 pass # TODO: This should warn or log or something
1037 1045 #-----------------------------------------------------------------------------
1038 1046 # Main entry point
1039 1047 #-----------------------------------------------------------------------------
1040 1048
1041 1049 launch_new_instance = NotebookApp.launch_instance
1042 1050
General Comments 0
You need to be logged in to leave comments. Login now