##// END OF EJS Templates
enh: added authentication ability for webapp
Satrajit Ghosh -
Show More
@@ -1,334 +1,357 b''
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 from tornado import web
20 20 from tornado import websocket
21 21
22 22 from zmq.eventloop import ioloop
23 23 from zmq.utils import jsonapi
24 24
25 25 from IPython.zmq.session import Session
26 26
27 27 try:
28 28 from docutils.core import publish_string
29 29 except ImportError:
30 30 publish_string = None
31 31
32 32
33 33
34 34 #-----------------------------------------------------------------------------
35 35 # Top-level handlers
36 36 #-----------------------------------------------------------------------------
37 37
38
39 class NBBrowserHandler(web.RequestHandler):
38 class BaseHandler(web.RequestHandler):
39 def get_current_user(self):
40 user_id = self.get_secure_cookie("user")
41 keyword = self.get_secure_cookie("keyword")
42 if self.application.keyword and self.application.keyword != keyword:
43 return None
44 if not user_id:
45 user_id = 'anonymous'
46 return user_id
47
48 class NBBrowserHandler(BaseHandler):
49 @web.authenticated
40 50 def get(self):
41 51 nbm = self.application.notebook_manager
42 52 project = nbm.notebook_dir
43 53 self.render('nbbrowser.html', project=project)
44 54
55 class LoginHandler(BaseHandler):
56 def get(self):
57 user_id = self.get_secure_cookie("user")
58 self.write('<html><body><form action="/login" method="post">'
59 'Name: <input type="text" name="name" value=%s>'
60 'Keyword: <input type="text" name="keyword">'
61 '<input type="submit" value="Sign in">'
62 '</form></body></html>'%user_id)
63
64 def post(self):
65 self.set_secure_cookie("user", self.get_argument("name", default=u''))
66 self.set_secure_cookie("keyword", self.get_argument("keyword", default=u''))
67 self.redirect("/")
45 68
46 69 class NewHandler(web.RequestHandler):
47 70 def get(self):
48 71 notebook_id = self.application.notebook_manager.new_notebook()
49 72 self.render('notebook.html', notebook_id=notebook_id)
50 73
51 74
52 75 class NamedNotebookHandler(web.RequestHandler):
53 76 def get(self, notebook_id):
54 77 nbm = self.application.notebook_manager
55 78 if not nbm.notebook_exists(notebook_id):
56 79 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
57 80 self.render('notebook.html', notebook_id=notebook_id)
58 81
59 82
60 83 #-----------------------------------------------------------------------------
61 84 # Kernel handlers
62 85 #-----------------------------------------------------------------------------
63 86
64 87
65 88 class MainKernelHandler(web.RequestHandler):
66 89
67 90 def get(self):
68 91 km = self.application.kernel_manager
69 92 self.finish(jsonapi.dumps(km.kernel_ids))
70 93
71 94 def post(self):
72 95 km = self.application.kernel_manager
73 96 notebook_id = self.get_argument('notebook', default=None)
74 97 kernel_id = km.start_kernel(notebook_id)
75 98 ws_url = self.application.ipython_app.get_ws_url()
76 99 data = {'ws_url':ws_url,'kernel_id':kernel_id}
77 100 self.set_header('Location', '/'+kernel_id)
78 101 self.finish(jsonapi.dumps(data))
79 102
80 103
81 104 class KernelHandler(web.RequestHandler):
82 105
83 106 SUPPORTED_METHODS = ('DELETE')
84 107
85 108 def delete(self, kernel_id):
86 109 km = self.application.kernel_manager
87 110 km.kill_kernel(kernel_id)
88 111 self.set_status(204)
89 112 self.finish()
90 113
91 114
92 115 class KernelActionHandler(web.RequestHandler):
93 116
94 117 def post(self, kernel_id, action):
95 118 km = self.application.kernel_manager
96 119 if action == 'interrupt':
97 120 km.interrupt_kernel(kernel_id)
98 121 self.set_status(204)
99 122 if action == 'restart':
100 123 new_kernel_id = km.restart_kernel(kernel_id)
101 124 ws_url = self.application.ipython_app.get_ws_url()
102 125 data = {'ws_url':ws_url,'kernel_id':new_kernel_id}
103 126 self.set_header('Location', '/'+new_kernel_id)
104 127 self.write(jsonapi.dumps(data))
105 128 self.finish()
106 129
107 130
108 131 class ZMQStreamHandler(websocket.WebSocketHandler):
109 132
110 133 def _reserialize_reply(self, msg_list):
111 134 """Reserialize a reply message using JSON.
112 135
113 136 This takes the msg list from the ZMQ socket, unserializes it using
114 137 self.session and then serializes the result using JSON. This method
115 138 should be used by self._on_zmq_reply to build messages that can
116 139 be sent back to the browser.
117 140 """
118 141 idents, msg_list = self.session.feed_identities(msg_list)
119 142 msg = self.session.unserialize(msg_list)
120 143 try:
121 144 msg['header'].pop('date')
122 145 except KeyError:
123 146 pass
124 147 try:
125 148 msg['parent_header'].pop('date')
126 149 except KeyError:
127 150 pass
128 151 msg.pop('buffers')
129 152 return jsonapi.dumps(msg)
130 153
131 154 def _on_zmq_reply(self, msg_list):
132 155 try:
133 156 msg = self._reserialize_reply(msg_list)
134 157 except:
135 158 self.application.kernel_manager.log.critical("Malformed message: %r" % msg_list)
136 159 else:
137 160 self.write_message(msg)
138 161
139 162
140 163 class IOPubHandler(ZMQStreamHandler):
141 164
142 165 def initialize(self, *args, **kwargs):
143 166 self._kernel_alive = True
144 167 self._beating = False
145 168 self.iopub_stream = None
146 169 self.hb_stream = None
147 170
148 171 def open(self, kernel_id):
149 172 km = self.application.kernel_manager
150 173 self.kernel_id = kernel_id
151 174 self.session = Session()
152 175 self.time_to_dead = km.time_to_dead
153 176 try:
154 177 self.iopub_stream = km.create_iopub_stream(kernel_id)
155 178 self.hb_stream = km.create_hb_stream(kernel_id)
156 179 except web.HTTPError:
157 180 # WebSockets don't response to traditional error codes so we
158 181 # close the connection.
159 182 if not self.stream.closed():
160 183 self.stream.close()
161 184 else:
162 185 self.iopub_stream.on_recv(self._on_zmq_reply)
163 186 self.start_hb(self.kernel_died)
164 187
165 188 def on_close(self):
166 189 # This method can be called twice, once by self.kernel_died and once
167 190 # from the WebSocket close event. If the WebSocket connection is
168 191 # closed before the ZMQ streams are setup, they could be None.
169 192 self.stop_hb()
170 193 if self.iopub_stream is not None and not self.iopub_stream.closed():
171 194 self.iopub_stream.on_recv(None)
172 195 self.iopub_stream.close()
173 196 if self.hb_stream is not None and not self.hb_stream.closed():
174 197 self.hb_stream.close()
175 198
176 199 def start_hb(self, callback):
177 200 """Start the heartbeating and call the callback if the kernel dies."""
178 201 if not self._beating:
179 202 self._kernel_alive = True
180 203
181 204 def ping_or_dead():
182 205 if self._kernel_alive:
183 206 self._kernel_alive = False
184 207 self.hb_stream.send(b'ping')
185 208 else:
186 209 try:
187 210 callback()
188 211 except:
189 212 pass
190 213 finally:
191 214 self._hb_periodic_callback.stop()
192 215
193 216 def beat_received(msg):
194 217 self._kernel_alive = True
195 218
196 219 self.hb_stream.on_recv(beat_received)
197 220 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
198 221 self._hb_periodic_callback.start()
199 222 self._beating= True
200 223
201 224 def stop_hb(self):
202 225 """Stop the heartbeating and cancel all related callbacks."""
203 226 if self._beating:
204 227 self._hb_periodic_callback.stop()
205 228 if not self.hb_stream.closed():
206 229 self.hb_stream.on_recv(None)
207 230
208 231 def kernel_died(self):
209 232 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
210 233 self.write_message(
211 234 {'header': {'msg_type': 'status'},
212 235 'parent_header': {},
213 236 'content': {'execution_state':'dead'}
214 237 }
215 238 )
216 239 self.on_close()
217 240
218 241
219 242 class ShellHandler(ZMQStreamHandler):
220 243
221 244 def initialize(self, *args, **kwargs):
222 245 self.shell_stream = None
223 246
224 247 def open(self, kernel_id):
225 248 km = self.application.kernel_manager
226 249 self.max_msg_size = km.max_msg_size
227 250 self.kernel_id = kernel_id
228 251 try:
229 252 self.shell_stream = km.create_shell_stream(kernel_id)
230 253 except web.HTTPError:
231 254 # WebSockets don't response to traditional error codes so we
232 255 # close the connection.
233 256 if not self.stream.closed():
234 257 self.stream.close()
235 258 else:
236 259 self.session = Session()
237 260 self.shell_stream.on_recv(self._on_zmq_reply)
238 261
239 262 def on_message(self, msg):
240 263 if len(msg) < self.max_msg_size:
241 264 msg = jsonapi.loads(msg)
242 265 self.session.send(self.shell_stream, msg)
243 266
244 267 def on_close(self):
245 268 # Make sure the stream exists and is not already closed.
246 269 if self.shell_stream is not None and not self.shell_stream.closed():
247 270 self.shell_stream.close()
248 271
249 272
250 273 #-----------------------------------------------------------------------------
251 274 # Notebook web service handlers
252 275 #-----------------------------------------------------------------------------
253 276
254 277 class NotebookRootHandler(web.RequestHandler):
255 278
256 279 def get(self):
257 280 nbm = self.application.notebook_manager
258 281 files = nbm.list_notebooks()
259 282 self.finish(jsonapi.dumps(files))
260 283
261 284 def post(self):
262 285 nbm = self.application.notebook_manager
263 286 body = self.request.body.strip()
264 287 format = self.get_argument('format', default='json')
265 288 name = self.get_argument('name', default=None)
266 289 if body:
267 290 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
268 291 else:
269 292 notebook_id = nbm.new_notebook()
270 293 self.set_header('Location', '/'+notebook_id)
271 294 self.finish(jsonapi.dumps(notebook_id))
272 295
273 296
274 297 class NotebookHandler(web.RequestHandler):
275 298
276 299 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
277 300
278 301 def get(self, notebook_id):
279 302 nbm = self.application.notebook_manager
280 303 format = self.get_argument('format', default='json')
281 304 last_mod, name, data = nbm.get_notebook(notebook_id, format)
282 305 if format == u'json':
283 306 self.set_header('Content-Type', 'application/json')
284 307 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
285 308 elif format == u'py':
286 309 self.set_header('Content-Type', 'application/x-python')
287 310 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
288 311 self.set_header('Last-Modified', last_mod)
289 312 self.finish(data)
290 313
291 314 def put(self, notebook_id):
292 315 nbm = self.application.notebook_manager
293 316 format = self.get_argument('format', default='json')
294 317 name = self.get_argument('name', default=None)
295 318 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
296 319 self.set_status(204)
297 320 self.finish()
298 321
299 322 def delete(self, notebook_id):
300 323 nbm = self.application.notebook_manager
301 324 nbm.delete_notebook(notebook_id)
302 325 self.set_status(204)
303 326 self.finish()
304 327
305 328 #-----------------------------------------------------------------------------
306 329 # RST web service handlers
307 330 #-----------------------------------------------------------------------------
308 331
309 332
310 333 class RSTHandler(web.RequestHandler):
311 334
312 335 def post(self):
313 336 if publish_string is None:
314 337 raise web.HTTPError(503, u'docutils not available')
315 338 body = self.request.body.strip()
316 339 source = body
317 340 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
318 341 defaults = {'file_insertion_enabled': 0,
319 342 'raw_enabled': 0,
320 343 '_disable_config': 1,
321 344 'stylesheet_path': 0
322 345 # 'template': template_path
323 346 }
324 347 try:
325 348 html = publish_string(source, writer_name='html',
326 349 settings_overrides=defaults
327 350 )
328 351 except:
329 352 raise web.HTTPError(400, u'Invalid RST')
330 353 print html
331 354 self.set_header('Content-Type', 'text/html')
332 355 self.finish(html)
333 356
334 357
@@ -1,280 +1,289 b''
1 1 """A tornado based IPython notebook server.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import errno
20 20 import logging
21 21 import os
22 22 import signal
23 23 import socket
24 24 import sys
25 25
26 26 import zmq
27 27
28 28 # Install the pyzmq ioloop. This has to be done before anything else from
29 29 # tornado is imported.
30 30 from zmq.eventloop import ioloop
31 31 import tornado.ioloop
32 32 tornado.ioloop = ioloop
33 33
34 34 from tornado import httpserver
35 35 from tornado import web
36 36
37 37 from .kernelmanager import MappingKernelManager
38 from .handlers import (
38 from .handlers import (LoginHandler,
39 39 NBBrowserHandler, NewHandler, NamedNotebookHandler,
40 40 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
41 41 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
42 42 )
43 43 from .notebookmanager import NotebookManager
44 44
45 45 from IPython.core.application import BaseIPythonApplication
46 46 from IPython.core.profiledir import ProfileDir
47 47 from IPython.zmq.session import Session
48 48 from IPython.zmq.zmqshell import ZMQInteractiveShell
49 49 from IPython.zmq.ipkernel import (
50 50 flags as ipkernel_flags,
51 51 aliases as ipkernel_aliases,
52 52 IPKernelApp
53 53 )
54 54 from IPython.utils.traitlets import Dict, Unicode, Int, List, Enum
55 55
56 56 #-----------------------------------------------------------------------------
57 57 # Module globals
58 58 #-----------------------------------------------------------------------------
59 59
60 60 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
61 61 _kernel_action_regex = r"(?P<action>restart|interrupt)"
62 62 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
63 63
64 64 LOCALHOST = '127.0.0.1'
65 65
66 66 _examples = """
67 67 ipython notebook # start the notebook
68 68 ipython notebook --profile=sympy # use the sympy profile
69 69 ipython notebook --pylab=inline # pylab in inline plotting mode
70 70 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
71 71 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
72 72 """
73 73
74 74 #-----------------------------------------------------------------------------
75 75 # The Tornado web application
76 76 #-----------------------------------------------------------------------------
77 77
78 78 class NotebookWebApplication(web.Application):
79 79
80 80 def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
81 81 handlers = [
82 82 (r"/", NBBrowserHandler),
83 (r"/login", LoginHandler),
83 84 (r"/new", NewHandler),
84 85 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
85 86 (r"/kernels", MainKernelHandler),
86 87 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
87 88 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
88 89 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
89 90 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
90 91 (r"/notebooks", NotebookRootHandler),
91 92 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
92 93 (r"/rstservice/render", RSTHandler)
93 94 ]
94 95 settings = dict(
95 96 template_path=os.path.join(os.path.dirname(__file__), "templates"),
96 97 static_path=os.path.join(os.path.dirname(__file__), "static"),
98 cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
99 login_url="/login",
97 100 )
98 101 web.Application.__init__(self, handlers, **settings)
99 102
100 103 self.kernel_manager = kernel_manager
101 104 self.log = log
102 105 self.notebook_manager = notebook_manager
103 106 self.ipython_app = ipython_app
104 107
105 108
106 109 #-----------------------------------------------------------------------------
107 110 # Aliases and Flags
108 111 #-----------------------------------------------------------------------------
109 112
110 113 flags = dict(ipkernel_flags)
111 114
112 115 # the flags that are specific to the frontend
113 116 # these must be scrubbed before being passed to the kernel,
114 117 # or it will raise an error on unrecognized flags
115 118 notebook_flags = []
116 119
117 120 aliases = dict(ipkernel_aliases)
118 121
119 122 aliases.update({
120 123 'ip': 'IPythonNotebookApp.ip',
121 124 'port': 'IPythonNotebookApp.port',
122 125 'keyfile': 'IPythonNotebookApp.keyfile',
123 126 'certfile': 'IPythonNotebookApp.certfile',
124 127 'ws-hostname': 'IPythonNotebookApp.ws_hostname',
125 'notebook-dir': 'NotebookManager.notebook_dir'
128 'notebook-dir': 'NotebookManager.notebook_dir',
129 'keyword' : 'IPythonNotebookApp.keyword'
126 130 })
127 131
128 132 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile', u'ws-hostname',
129 133 u'notebook-dir']
130 134
131 135 #-----------------------------------------------------------------------------
132 136 # IPythonNotebookApp
133 137 #-----------------------------------------------------------------------------
134 138
135 139 class IPythonNotebookApp(BaseIPythonApplication):
136 140
137 141 name = 'ipython-notebook'
138 142 default_config_file_name='ipython_notebook_config.py'
139 143
140 144 description = """
141 145 The IPython HTML Notebook.
142 146
143 147 This launches a Tornado based HTML Notebook Server that serves up an
144 148 HTML5/Javascript Notebook client.
145 149 """
146 150 examples = _examples
147 151
148 152 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
149 153 MappingKernelManager, NotebookManager]
150 154 flags = Dict(flags)
151 155 aliases = Dict(aliases)
152 156
153 157 kernel_argv = List(Unicode)
154 158
155 159 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
156 160 default_value=logging.INFO,
157 161 config=True,
158 162 help="Set the log level by value or name.")
159 163
160 164 # Network related information.
161 165
162 166 ip = Unicode(LOCALHOST, config=True,
163 167 help="The IP address the notebook server will listen on."
164 168 )
165 169
166 170 def _ip_changed(self, name, old, new):
167 171 if new == u'*': self.ip = u''
168 172
169 173 port = Int(8888, config=True,
170 174 help="The port the notebook server will listen on."
171 175 )
172 176
173 177 ws_hostname = Unicode(LOCALHOST, config=True,
174 178 help="""The FQDN or IP for WebSocket connections. The default will work
175 179 fine when the server is listening on localhost, but this needs to
176 180 be set if the ip option is used. It will be used as the hostname part
177 181 of the WebSocket url: ws://hostname/path."""
178 182 )
179 183
180 184 certfile = Unicode(u'', config=True,
181 185 help="""The full path to an SSL/TLS certificate file."""
182 186 )
183 187
184 188 keyfile = Unicode(u'', config=True,
185 189 help="""The full path to a private key file for usage with SSL/TLS."""
186 190 )
187 191
192 keyword = Unicode(u'', config=True,
193 help="""Keyword to use for web authentication"""
194 )
195
188 196 def get_ws_url(self):
189 197 """Return the WebSocket URL for this server."""
190 198 if self.certfile:
191 199 prefix = u'wss://'
192 200 else:
193 201 prefix = u'ws://'
194 202 return prefix + self.ws_hostname + u':' + unicode(self.port)
195 203
196 204 def parse_command_line(self, argv=None):
197 205 super(IPythonNotebookApp, self).parse_command_line(argv)
198 206 if argv is None:
199 207 argv = sys.argv[1:]
200 208
201 209 self.kernel_argv = list(argv) # copy
202 210 # Kernel should inherit default config file from frontend
203 211 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
204 212 # Scrub frontend-specific flags
205 213 for a in argv:
206 214 if a.startswith('-') and a.lstrip('-') in notebook_flags:
207 215 self.kernel_argv.remove(a)
208 216 for a in argv:
209 217 if a.startswith('-'):
210 218 alias = a.lstrip('-').split('=')[0]
211 219 if alias in notebook_aliases:
212 220 self.kernel_argv.remove(a)
213 221
214 222 def init_configurables(self):
215 223 # Don't let Qt or ZMQ swallow KeyboardInterupts.
216 224 signal.signal(signal.SIGINT, signal.SIG_DFL)
217 225
218 226 # Create a KernelManager and start a kernel.
219 227 self.kernel_manager = MappingKernelManager(
220 228 config=self.config, log=self.log, kernel_argv=self.kernel_argv
221 229 )
222 230 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
223 231 self.notebook_manager.list_notebooks()
224 232
225 233 def init_logging(self):
226 234 super(IPythonNotebookApp, self).init_logging()
227 235 # This prevents double log messages because tornado use a root logger that
228 236 # self.log is a child of. The logging module dipatches log messages to a log
229 237 # and all of its ancenstors until propagate is set to False.
230 238 self.log.propagate = False
231 239
232 240 def initialize(self, argv=None):
233 241 super(IPythonNotebookApp, self).initialize(argv)
234 242 self.init_configurables()
235 243 self.web_app = NotebookWebApplication(
236 244 self, self.kernel_manager, self.notebook_manager, self.log
237 245 )
238 246 if self.certfile:
239 247 ssl_options = dict(certfile=self.certfile)
240 248 if self.keyfile:
241 249 ssl_options['keyfile'] = self.keyfile
242 250 else:
243 251 ssl_options = None
252 self.web_app.keyword = self.keyword
244 253 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
245 254 if ssl_options is None and not self.ip:
246 255 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
247 256 'but not using any encryption or authentication. This is highly '
248 257 'insecure and not recommended.')
249 258
250 259 # Try random ports centered around the default.
251 260 from random import randint
252 261 n = 50 # Max number of attempts, keep reasonably large.
253 262 for port in [self.port] + [self.port + randint(-2*n, 2*n) for i in range(n)]:
254 263 try:
255 264 self.http_server.listen(port, self.ip)
256 265 except socket.error, e:
257 266 if e.errno != errno.EADDRINUSE:
258 267 raise
259 268 self.log.info('The port %i is already in use, trying another random port.' % port)
260 269 else:
261 270 self.port = port
262 271 break
263 272
264 273 def start(self):
265 274 ip = self.ip if self.ip else '[all ip addresses on your system]'
266 275 proto = 'https' if self.certfile else 'http'
267 276 self.log.info("The IPython Notebook is running at: %s://%s:%i" % (proto,
268 277 ip,
269 278 self.port))
270 279 ioloop.IOLoop.instance().start()
271 280
272 281 #-----------------------------------------------------------------------------
273 282 # Main entry point
274 283 #-----------------------------------------------------------------------------
275 284
276 285 def launch_new_instance():
277 286 app = IPythonNotebookApp()
278 287 app.initialize()
279 288 app.start()
280 289
General Comments 0
You need to be logged in to leave comments. Login now