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