##// END OF EJS Templates
print info string on interrupt, log it on startup
Paul Ivanov -
Show More
@@ -1,143 +1,143 b''
1 1 """A notebook manager that uses Azure blob storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2012 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 datetime
20 20
21 21 import azure
22 22 from azure.storage import BlobService
23 23
24 24 from tornado import web
25 25
26 26 from .nbmanager import NotebookManager
27 27 from IPython.nbformat import current
28 28 from IPython.utils.traitlets import Unicode, Instance
29 29
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34
35 35 class AzureNotebookManager(NotebookManager):
36 36
37 37 account_name = Unicode('', config=True, help='Azure storage account name.')
38 38 account_key = Unicode('', config=True, help='Azure storage account key.')
39 39 container = Unicode('', config=True, help='Container name for notebooks.')
40 40
41 41 blob_service_host_base = Unicode('.blob.core.windows.net', config=True,
42 42 help='The basename for the blob service URL. If running on the preview site this '
43 43 'will be .blob.core.azure-preview.com.')
44 44 def _blob_service_host_base_changed(self, new):
45 45 self._update_service_host_base(new)
46 46
47 47 blob_service = Instance('azure.storage.BlobService')
48 48 def _blob_service_default(self):
49 49 return BlobService(account_name=self.account_name, account_key=self.account_key)
50 50
51 51 def __init__(self, **kwargs):
52 52 super(AzureNotebookManager, self).__init__(**kwargs)
53 53 self._update_service_host_base(self.blob_service_host_base)
54 54 self._create_container()
55 55
56 56 def _update_service_host_base(self, shb):
57 57 azure.BLOB_SERVICE_HOST_BASE = shb
58 58
59 59 def _create_container(self):
60 60 self.blob_service.create_container(self.container)
61 61
62 62 def load_notebook_names(self):
63 63 """On startup load the notebook ids and names from Azure.
64 64
65 65 The blob names are the notebook ids and the notebook names are stored
66 66 as blob metadata.
67 67 """
68 68 self.mapping = {}
69 69 blobs = self.blob_service.list_blobs(self.container)
70 70 ids = [blob.name for blob in blobs]
71 71
72 72 for id in ids:
73 73 md = self.blob_service.get_blob_metadata(self.container, id)
74 74 name = md['x-ms-meta-nbname']
75 75 self.mapping[id] = name
76 76
77 77 def list_notebooks(self):
78 78 """List all notebooks in the container.
79 79
80 80 This version uses `self.mapping` as the authoritative notebook list.
81 81 """
82 82 data = [dict(notebook_id=id,name=name) for id, name in self.mapping.items()]
83 83 data = sorted(data, key=lambda item: item['name'])
84 84 return data
85 85
86 86 def read_notebook_object(self, notebook_id):
87 87 """Get the object representation of a notebook by notebook_id."""
88 88 if not self.notebook_exists(notebook_id):
89 89 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
90 90 try:
91 91 s = self.blob_service.get_blob(self.container, notebook_id)
92 92 except:
93 93 raise web.HTTPError(500, u'Notebook cannot be read.')
94 94 try:
95 95 # v1 and v2 and json in the .ipynb files.
96 96 nb = current.reads(s, u'json')
97 97 except:
98 98 raise web.HTTPError(500, u'Unreadable JSON notebook.')
99 99 # Todo: The last modified should actually be saved in the notebook document.
100 100 # We are just using the current datetime until that is implemented.
101 101 last_modified = datetime.datetime.utcnow()
102 102 return last_modified, nb
103 103
104 104 def write_notebook_object(self, nb, notebook_id=None):
105 105 """Save an existing notebook object by notebook_id."""
106 106 try:
107 107 new_name = nb.metadata.name
108 108 except AttributeError:
109 109 raise web.HTTPError(400, u'Missing notebook name')
110 110
111 111 if notebook_id is None:
112 112 notebook_id = self.new_notebook_id(new_name)
113 113
114 114 if notebook_id not in self.mapping:
115 115 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
116 116
117 117 try:
118 118 data = current.writes(nb, u'json')
119 119 except Exception as e:
120 120 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
121 121
122 122 metadata = {'nbname': new_name}
123 123 try:
124 124 self.blob_service.put_blob(self.container, notebook_id, data, 'BlockBlob', x_ms_meta_name_values=metadata)
125 125 except Exception as e:
126 126 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
127 127
128 128 self.mapping[notebook_id] = new_name
129 129 return notebook_id
130 130
131 131 def delete_notebook(self, notebook_id):
132 132 """Delete notebook by notebook_id."""
133 133 if not self.notebook_exists(notebook_id):
134 134 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
135 135 try:
136 136 self.blob_service.delete_blob(self.container, notebook_id)
137 137 except Exception as e:
138 138 raise web.HTTPError(400, u'Unexpected error while deleting notebook: %s' % e)
139 139 else:
140 140 self.delete_notebook_id(notebook_id)
141 141
142 def log_info(self):
143 self.log.info("Serving notebooks from Azure storage: %s, %s", self.account_name, self.container)
142 def info_string(self):
143 return "Serving notebooks from Azure storage: %s, %s" % (self.account_name, self.container)
@@ -1,196 +1,196 b''
1 1 """A notebook manager that uses the local file system for storage.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 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 datetime
20 20 import io
21 21 import os
22 22 import glob
23 23
24 24 from tornado import web
25 25
26 26 from .nbmanager import NotebookManager
27 27 from IPython.nbformat import current
28 28 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Classes
32 32 #-----------------------------------------------------------------------------
33 33
34 34 class FileNotebookManager(NotebookManager):
35 35
36 36 save_script = Bool(False, config=True,
37 37 help="""Automatically create a Python script when saving the notebook.
38 38
39 39 For easier use of import, %run and %load across notebooks, a
40 40 <notebook-name>.py script will be created next to any
41 41 <notebook-name>.ipynb on each save. This can also be set with the
42 42 short `--script` flag.
43 43 """
44 44 )
45 45
46 46 filename_ext = Unicode(u'.ipynb')
47 47
48 48 # Map notebook names to notebook_ids
49 49 rev_mapping = Dict()
50 50
51 51 def get_notebook_names(self):
52 52 """List all notebook names in the notebook dir."""
53 53 names = glob.glob(os.path.join(self.notebook_dir,
54 54 '*' + self.filename_ext))
55 55 names = [os.path.splitext(os.path.basename(name))[0]
56 56 for name in names]
57 57 return names
58 58
59 59 def list_notebooks(self):
60 60 """List all notebooks in the notebook dir."""
61 61 names = self.get_notebook_names()
62 62
63 63 data = []
64 64 for name in names:
65 65 if name not in self.rev_mapping:
66 66 notebook_id = self.new_notebook_id(name)
67 67 else:
68 68 notebook_id = self.rev_mapping[name]
69 69 data.append(dict(notebook_id=notebook_id,name=name))
70 70 data = sorted(data, key=lambda item: item['name'])
71 71 return data
72 72
73 73 def new_notebook_id(self, name):
74 74 """Generate a new notebook_id for a name and store its mappings."""
75 75 notebook_id = super(FileNotebookManager, self).new_notebook_id(name)
76 76 self.rev_mapping[name] = notebook_id
77 77 return notebook_id
78 78
79 79 def delete_notebook_id(self, notebook_id):
80 80 """Delete a notebook's id in the mapping."""
81 81 name = self.mapping[notebook_id]
82 82 super(FileNotebookManager, self).delete_notebook_id(notebook_id)
83 83 del self.rev_mapping[name]
84 84
85 85 def notebook_exists(self, notebook_id):
86 86 """Does a notebook exist?"""
87 87 exists = super(FileNotebookManager, self).notebook_exists(notebook_id)
88 88 if not exists:
89 89 return False
90 90 path = self.get_path_by_name(self.mapping[notebook_id])
91 91 return os.path.isfile(path)
92 92
93 93 def find_path(self, notebook_id):
94 94 """Return a full path to a notebook given its notebook_id."""
95 95 try:
96 96 name = self.mapping[notebook_id]
97 97 except KeyError:
98 98 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
99 99 return self.get_path_by_name(name)
100 100
101 101 def get_path_by_name(self, name):
102 102 """Return a full path to a notebook given its name."""
103 103 filename = name + self.filename_ext
104 104 path = os.path.join(self.notebook_dir, filename)
105 105 return path
106 106
107 107 def read_notebook_object(self, notebook_id):
108 108 """Get the NotebookNode representation of a notebook by notebook_id."""
109 109 path = self.find_path(notebook_id)
110 110 if not os.path.isfile(path):
111 111 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
112 112 info = os.stat(path)
113 113 last_modified = datetime.datetime.utcfromtimestamp(info.st_mtime)
114 114 with open(path,'r') as f:
115 115 s = f.read()
116 116 try:
117 117 # v1 and v2 and json in the .ipynb files.
118 118 nb = current.reads(s, u'json')
119 119 except:
120 120 raise web.HTTPError(500, u'Unreadable JSON notebook.')
121 121 # Always use the filename as the notebook name.
122 122 nb.metadata.name = os.path.splitext(os.path.basename(path))[0]
123 123 return last_modified, nb
124 124
125 125 def write_notebook_object(self, nb, notebook_id=None):
126 126 """Save an existing notebook object by notebook_id."""
127 127 try:
128 128 new_name = nb.metadata.name
129 129 except AttributeError:
130 130 raise web.HTTPError(400, u'Missing notebook name')
131 131
132 132 if notebook_id is None:
133 133 notebook_id = self.new_notebook_id(new_name)
134 134
135 135 if notebook_id not in self.mapping:
136 136 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
137 137
138 138 old_name = self.mapping[notebook_id]
139 139 path = self.get_path_by_name(new_name)
140 140 try:
141 141 with open(path,'w') as f:
142 142 current.write(nb, f, u'json')
143 143 except Exception as e:
144 144 raise web.HTTPError(400, u'Unexpected error while saving notebook: %s' % e)
145 145
146 146 # save .py script as well
147 147 if self.save_script:
148 148 pypath = os.path.splitext(path)[0] + '.py'
149 149 try:
150 150 with io.open(pypath,'w', encoding='utf-8') as f:
151 151 current.write(nb, f, u'py')
152 152 except Exception as e:
153 153 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s' % e)
154 154
155 155 # remove old files if the name changed
156 156 if old_name != new_name:
157 157 old_path = self.get_path_by_name(old_name)
158 158 if os.path.isfile(old_path):
159 159 os.unlink(old_path)
160 160 if self.save_script:
161 161 old_pypath = os.path.splitext(old_path)[0] + '.py'
162 162 if os.path.isfile(old_pypath):
163 163 os.unlink(old_pypath)
164 164 self.mapping[notebook_id] = new_name
165 165 self.rev_mapping[new_name] = notebook_id
166 166 del self.rev_mapping[old_name]
167 167
168 168 return notebook_id
169 169
170 170 def delete_notebook(self, notebook_id):
171 171 """Delete notebook by notebook_id."""
172 172 path = self.find_path(notebook_id)
173 173 if not os.path.isfile(path):
174 174 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
175 175 os.unlink(path)
176 176 self.delete_notebook_id(notebook_id)
177 177
178 178 def increment_filename(self, basename):
179 179 """Return a non-used filename of the form basename<int>.
180 180
181 181 This searches through the filenames (basename0, basename1, ...)
182 182 until is find one that is not already being used. It is used to
183 183 create Untitled and Copy names that are unique.
184 184 """
185 185 i = 0
186 186 while True:
187 187 name = u'%s%i' % (basename,i)
188 188 path = self.get_path_by_name(name)
189 189 if not os.path.isfile(path):
190 190 break
191 191 else:
192 192 i = i+1
193 193 return name
194
195 def log_info(self):
196 self.log.info("Serving notebooks from local directory: %s", self.notebook_dir)
194
195 def info_string(self):
196 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,210 +1,214 b''
1 1 """A base class notebook manager.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 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 os
20 20 import uuid
21 21
22 22 from tornado import web
23 23
24 24 from IPython.config.configurable import LoggingConfigurable
25 25 from IPython.nbformat import current
26 26 from IPython.utils.traitlets import List, Dict, Unicode, TraitError
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Classes
30 30 #-----------------------------------------------------------------------------
31 31
32 32 class NotebookManager(LoggingConfigurable):
33 33
34 34 # Todo:
35 35 # The notebook_dir attribute is used to mean a couple of different things:
36 36 # 1. Where the notebooks are stored if FileNotebookManager is used.
37 37 # 2. The cwd of the kernel for a project.
38 38 # Right now we use this attribute in a number of different places and
39 39 # we are going to have to disentagle all of this.
40 40 notebook_dir = Unicode(os.getcwdu(), config=True, help="""
41 41 The directory to use for notebooks.
42 42 """)
43 43 def _notebook_dir_changed(self, name, old, new):
44 44 """do a bit of validation of the notebook dir"""
45 45 if not os.path.isabs(new):
46 46 # If we receive a non-absolute path, make it absolute.
47 47 abs_new = os.path.abspath(new)
48 48 self.notebook_dir = abs_new
49 49 return
50 50 if os.path.exists(new) and not os.path.isdir(new):
51 51 raise TraitError("notebook dir %r is not a directory" % new)
52 52 if not os.path.exists(new):
53 53 self.log.info("Creating notebook dir %s", new)
54 54 try:
55 55 os.mkdir(new)
56 56 except:
57 57 raise TraitError("Couldn't create notebook dir %r" % new)
58 58
59 59 allowed_formats = List([u'json',u'py'])
60 60
61 61 # Map notebook_ids to notebook names
62 62 mapping = Dict()
63 63
64 64 def load_notebook_names(self):
65 65 """Load the notebook names into memory.
66 66
67 67 This should be called once immediately after the notebook manager
68 68 is created to load the existing notebooks into the mapping in
69 69 memory.
70 70 """
71 71 self.list_notebooks()
72 72
73 73 def list_notebooks(self):
74 74 """List all notebooks.
75 75
76 76 This returns a list of dicts, each of the form::
77 77
78 78 dict(notebook_id=notebook,name=name)
79 79
80 80 This list of dicts should be sorted by name::
81 81
82 82 data = sorted(data, key=lambda item: item['name'])
83 83 """
84 84 raise NotImplementedError('must be implemented in a subclass')
85 85
86 86
87 87 def new_notebook_id(self, name):
88 88 """Generate a new notebook_id for a name and store its mapping."""
89 89 # TODO: the following will give stable urls for notebooks, but unless
90 90 # the notebooks are immediately redirected to their new urls when their
91 91 # filemname changes, nasty inconsistencies result. So for now it's
92 92 # disabled and instead we use a random uuid4() call. But we leave the
93 93 # logic here so that we can later reactivate it, whhen the necessary
94 94 # url redirection code is written.
95 95 #notebook_id = unicode(uuid.uuid5(uuid.NAMESPACE_URL,
96 96 # 'file://'+self.get_path_by_name(name).encode('utf-8')))
97 97
98 98 notebook_id = unicode(uuid.uuid4())
99 99 self.mapping[notebook_id] = name
100 100 return notebook_id
101 101
102 102 def delete_notebook_id(self, notebook_id):
103 103 """Delete a notebook's id in the mapping.
104 104
105 105 This doesn't delete the actual notebook, only its entry in the mapping.
106 106 """
107 107 del self.mapping[notebook_id]
108 108
109 109 def notebook_exists(self, notebook_id):
110 110 """Does a notebook exist?"""
111 111 return notebook_id in self.mapping
112 112
113 113 def get_notebook(self, notebook_id, format=u'json'):
114 114 """Get the representation of a notebook in format by notebook_id."""
115 115 format = unicode(format)
116 116 if format not in self.allowed_formats:
117 117 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
118 118 last_modified, nb = self.read_notebook_object(notebook_id)
119 119 kwargs = {}
120 120 if format == 'json':
121 121 # don't split lines for sending over the wire, because it
122 122 # should match the Python in-memory format.
123 123 kwargs['split_lines'] = False
124 124 data = current.writes(nb, format, **kwargs)
125 125 name = nb.metadata.get('name','notebook')
126 126 return last_modified, name, data
127 127
128 128 def read_notebook_object(self, notebook_id):
129 129 """Get the object representation of a notebook by notebook_id."""
130 130 raise NotImplementedError('must be implemented in a subclass')
131 131
132 132 def save_new_notebook(self, data, name=None, format=u'json'):
133 133 """Save a new notebook and return its notebook_id.
134 134
135 135 If a name is passed in, it overrides any values in the notebook data
136 136 and the value in the data is updated to use that value.
137 137 """
138 138 if format not in self.allowed_formats:
139 139 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
140 140
141 141 try:
142 142 nb = current.reads(data.decode('utf-8'), format)
143 143 except:
144 144 raise web.HTTPError(400, u'Invalid JSON data')
145 145
146 146 if name is None:
147 147 try:
148 148 name = nb.metadata.name
149 149 except AttributeError:
150 150 raise web.HTTPError(400, u'Missing notebook name')
151 151 nb.metadata.name = name
152 152
153 153 notebook_id = self.write_notebook_object(nb)
154 154 return notebook_id
155 155
156 156 def save_notebook(self, notebook_id, data, name=None, format=u'json'):
157 157 """Save an existing notebook by notebook_id."""
158 158 if format not in self.allowed_formats:
159 159 raise web.HTTPError(415, u'Invalid notebook format: %s' % format)
160 160
161 161 try:
162 162 nb = current.reads(data.decode('utf-8'), format)
163 163 except:
164 164 raise web.HTTPError(400, u'Invalid JSON data')
165 165
166 166 if name is not None:
167 167 nb.metadata.name = name
168 168 self.write_notebook_object(nb, notebook_id)
169 169
170 170 def write_notebook_object(self, nb, notebook_id=None):
171 171 """Write a notebook object and return its notebook_id.
172 172
173 173 If notebook_id is None, this method should create a new notebook_id.
174 174 If notebook_id is not None, this method should check to make sure it
175 175 exists and is valid.
176 176 """
177 177 raise NotImplementedError('must be implemented in a subclass')
178 178
179 179 def delete_notebook(self, notebook_id):
180 180 """Delete notebook by notebook_id."""
181 181 raise NotImplementedError('must be implemented in a subclass')
182 182
183 183 def increment_filename(self, name):
184 184 """Increment a filename to make it unique.
185 185
186 186 This exists for notebook stores that must have unique names. When a notebook
187 187 is created or copied this method constructs a unique filename, typically
188 188 by appending an integer to the name.
189 189 """
190 190 return name
191 191
192 192 def new_notebook(self):
193 193 """Create a new notebook and return its notebook_id."""
194 194 name = self.increment_filename('Untitled')
195 195 metadata = current.new_metadata(name=name)
196 196 nb = current.new_notebook(metadata=metadata)
197 197 notebook_id = self.write_notebook_object(nb)
198 198 return notebook_id
199 199
200 200 def copy_notebook(self, notebook_id):
201 201 """Copy an existing notebook and return its notebook_id."""
202 202 last_mod, nb = self.read_notebook_object(notebook_id)
203 203 name = nb.metadata.name + '-Copy'
204 204 name = self.increment_filename(name)
205 205 nb.metadata.name = name
206 206 notebook_id = self.write_notebook_object(nb)
207 207 return notebook_id
208 208
209 209 def log_info(self):
210 self.log.info("Serving notebooks") No newline at end of file
210 self.log.info(self.info_string())
211
212 def info_string(self):
213 return "Serving notebooks"
214
@@ -1,678 +1,679 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
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 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import re
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import uuid
32 32 import webbrowser
33 33
34 34 # Third party
35 35 import zmq
36 36 from jinja2 import Environment, FileSystemLoader
37 37
38 38 # Install the pyzmq ioloop. This has to be done before anything else from
39 39 # tornado is imported.
40 40 from zmq.eventloop import ioloop
41 41 ioloop.install()
42 42
43 43 from tornado import httpserver
44 44 from tornado import web
45 45
46 46 # Our own libraries
47 47 from .kernelmanager import MappingKernelManager
48 48 from .handlers import (LoginHandler, LogoutHandler,
49 49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
54 54 FileFindHandler,
55 55 )
56 56 from .nbmanager import NotebookManager
57 57 from .filenbmanager import FileNotebookManager
58 58 from .clustermanager import ClusterManager
59 59
60 60 from IPython.config.application import catch_config_error, boolean_flag
61 61 from IPython.core.application import BaseIPythonApplication
62 62 from IPython.core.profiledir import ProfileDir
63 63 from IPython.frontend.consoleapp import IPythonConsoleApp
64 64 from IPython.kernel import swallow_argv
65 65 from IPython.kernel.zmq.session import Session, default_secure
66 66 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
67 67 from IPython.kernel.zmq.kernelapp import (
68 68 kernel_flags,
69 69 kernel_aliases,
70 70 IPKernelApp
71 71 )
72 72 from IPython.utils.importstring import import_item
73 73 from IPython.utils.localinterfaces import LOCALHOST
74 74 from IPython.utils.traitlets import (
75 75 Dict, Unicode, Integer, List, Enum, Bool,
76 76 DottedObjectName
77 77 )
78 78 from IPython.utils import py3compat
79 79 from IPython.utils.path import filefind
80 80
81 81 #-----------------------------------------------------------------------------
82 82 # Module globals
83 83 #-----------------------------------------------------------------------------
84 84
85 85 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
86 86 _kernel_action_regex = r"(?P<action>restart|interrupt)"
87 87 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
88 88 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
89 89 _cluster_action_regex = r"(?P<action>start|stop)"
90 90
91 91 _examples = """
92 92 ipython notebook # start the notebook
93 93 ipython notebook --profile=sympy # use the sympy profile
94 94 ipython notebook --pylab=inline # pylab in inline plotting mode
95 95 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
96 96 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
97 97 """
98 98
99 99 # Packagers: modify this line if you store the notebook static files elsewhere
100 100 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
101 101
102 102 #-----------------------------------------------------------------------------
103 103 # Helper functions
104 104 #-----------------------------------------------------------------------------
105 105
106 106 def url_path_join(a,b):
107 107 if a.endswith('/') and b.startswith('/'):
108 108 return a[:-1]+b
109 109 else:
110 110 return a+b
111 111
112 112 def random_ports(port, n):
113 113 """Generate a list of n random ports near the given port.
114 114
115 115 The first 5 ports will be sequential, and the remaining n-5 will be
116 116 randomly selected in the range [port-2*n, port+2*n].
117 117 """
118 118 for i in range(min(5, n)):
119 119 yield port + i
120 120 for i in range(n-5):
121 121 yield port + random.randint(-2*n, 2*n)
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, notebook_manager,
130 130 cluster_manager, log,
131 131 base_project_url, settings_overrides):
132 132 handlers = [
133 133 (r"/", ProjectDashboardHandler),
134 134 (r"/login", LoginHandler),
135 135 (r"/logout", LogoutHandler),
136 136 (r"/new", NewHandler),
137 137 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
138 138 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
139 139 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
140 140 (r"/kernels", MainKernelHandler),
141 141 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
142 142 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
143 143 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
144 144 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
145 145 (r"/notebooks", NotebookRootHandler),
146 146 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
147 147 (r"/rstservice/render", RSTHandler),
148 148 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
149 149 (r"/clusters", MainClusterHandler),
150 150 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
151 151 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
152 152 ]
153 153
154 154 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
155 155 # base_project_url will always be unicode, which will in turn
156 156 # make the patterns unicode, and ultimately result in unicode
157 157 # keys in kwargs to handler._execute(**kwargs) in tornado.
158 158 # This enforces that base_project_url be ascii in that situation.
159 159 #
160 160 # Note that the URLs these patterns check against are escaped,
161 161 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
162 162 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
163 163
164 164 settings = dict(
165 165 template_path=os.path.join(os.path.dirname(__file__), "templates"),
166 166 static_path=ipython_app.static_file_path,
167 167 static_handler_class = FileFindHandler,
168 168 static_url_prefix = url_path_join(base_project_url,'/static/'),
169 169 cookie_secret=os.urandom(1024),
170 170 login_url=url_path_join(base_project_url,'/login'),
171 171 cookie_name='username-%s' % uuid.uuid4(),
172 172 )
173 173
174 174 # allow custom overrides for the tornado web app.
175 175 settings.update(settings_overrides)
176 176
177 177 # prepend base_project_url onto the patterns that we match
178 178 new_handlers = []
179 179 for handler in handlers:
180 180 pattern = url_path_join(base_project_url, handler[0])
181 181 new_handler = tuple([pattern]+list(handler[1:]))
182 182 new_handlers.append( new_handler )
183 183
184 184 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
185 185
186 186 self.kernel_manager = kernel_manager
187 187 self.notebook_manager = notebook_manager
188 188 self.cluster_manager = cluster_manager
189 189 self.ipython_app = ipython_app
190 190 self.read_only = self.ipython_app.read_only
191 191 self.config = self.ipython_app.config
192 192 self.use_less = self.ipython_app.use_less
193 193 self.log = log
194 194 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
195 195
196 196
197 197
198 198 #-----------------------------------------------------------------------------
199 199 # Aliases and Flags
200 200 #-----------------------------------------------------------------------------
201 201
202 202 flags = dict(kernel_flags)
203 203 flags['no-browser']=(
204 204 {'NotebookApp' : {'open_browser' : False}},
205 205 "Don't open the notebook in a browser after startup."
206 206 )
207 207 flags['no-mathjax']=(
208 208 {'NotebookApp' : {'enable_mathjax' : False}},
209 209 """Disable MathJax
210 210
211 211 MathJax is the javascript library IPython uses to render math/LaTeX. It is
212 212 very large, so you may want to disable it if you have a slow internet
213 213 connection, or for offline use of the notebook.
214 214
215 215 When disabled, equations etc. will appear as their untransformed TeX source.
216 216 """
217 217 )
218 218 flags['read-only'] = (
219 219 {'NotebookApp' : {'read_only' : True}},
220 220 """Allow read-only access to notebooks.
221 221
222 222 When using a password to protect the notebook server, this flag
223 223 allows unauthenticated clients to view the notebook list, and
224 224 individual notebooks, but not edit them, start kernels, or run
225 225 code.
226 226
227 227 If no password is set, the server will be entirely read-only.
228 228 """
229 229 )
230 230
231 231 # Add notebook manager flags
232 232 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
233 233 'Auto-save a .py script everytime the .ipynb notebook is saved',
234 234 'Do not auto-save .py scripts for every notebook'))
235 235
236 236 # the flags that are specific to the frontend
237 237 # these must be scrubbed before being passed to the kernel,
238 238 # or it will raise an error on unrecognized flags
239 239 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
240 240
241 241 aliases = dict(kernel_aliases)
242 242
243 243 aliases.update({
244 244 'ip': 'NotebookApp.ip',
245 245 'port': 'NotebookApp.port',
246 246 'port-retries': 'NotebookApp.port_retries',
247 247 'transport': 'KernelManager.transport',
248 248 'keyfile': 'NotebookApp.keyfile',
249 249 'certfile': 'NotebookApp.certfile',
250 250 'notebook-dir': 'NotebookManager.notebook_dir',
251 251 'browser': 'NotebookApp.browser',
252 252 })
253 253
254 254 # remove ipkernel flags that are singletons, and don't make sense in
255 255 # multi-kernel evironment:
256 256 aliases.pop('f', None)
257 257
258 258 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
259 259 u'notebook-dir']
260 260
261 261 #-----------------------------------------------------------------------------
262 262 # NotebookApp
263 263 #-----------------------------------------------------------------------------
264 264
265 265 class NotebookApp(BaseIPythonApplication):
266 266
267 267 name = 'ipython-notebook'
268 268 default_config_file_name='ipython_notebook_config.py'
269 269
270 270 description = """
271 271 The IPython HTML Notebook.
272 272
273 273 This launches a Tornado based HTML Notebook Server that serves up an
274 274 HTML5/Javascript Notebook client.
275 275 """
276 276 examples = _examples
277 277
278 278 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
279 279 FileNotebookManager]
280 280 flags = Dict(flags)
281 281 aliases = Dict(aliases)
282 282
283 283 kernel_argv = List(Unicode)
284 284
285 285 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
286 286 default_value=logging.INFO,
287 287 config=True,
288 288 help="Set the log level by value or name.")
289 289
290 290 # create requested profiles by default, if they don't exist:
291 291 auto_create = Bool(True)
292 292
293 293 # file to be opened in the notebook server
294 294 file_to_run = Unicode('')
295 295
296 296 # Network related information.
297 297
298 298 ip = Unicode(LOCALHOST, config=True,
299 299 help="The IP address the notebook server will listen on."
300 300 )
301 301
302 302 def _ip_changed(self, name, old, new):
303 303 if new == u'*': self.ip = u''
304 304
305 305 port = Integer(8888, config=True,
306 306 help="The port the notebook server will listen on."
307 307 )
308 308 port_retries = Integer(50, config=True,
309 309 help="The number of additional ports to try if the specified port is not available."
310 310 )
311 311
312 312 certfile = Unicode(u'', config=True,
313 313 help="""The full path to an SSL/TLS certificate file."""
314 314 )
315 315
316 316 keyfile = Unicode(u'', config=True,
317 317 help="""The full path to a private key file for usage with SSL/TLS."""
318 318 )
319 319
320 320 password = Unicode(u'', config=True,
321 321 help="""Hashed password to use for web authentication.
322 322
323 323 To generate, type in a python/IPython shell:
324 324
325 325 from IPython.lib import passwd; passwd()
326 326
327 327 The string should be of the form type:salt:hashed-password.
328 328 """
329 329 )
330 330
331 331 open_browser = Bool(True, config=True,
332 332 help="""Whether to open in a browser after starting.
333 333 The specific browser used is platform dependent and
334 334 determined by the python standard library `webbrowser`
335 335 module, unless it is overridden using the --browser
336 336 (NotebookApp.browser) configuration option.
337 337 """)
338 338
339 339 browser = Unicode(u'', config=True,
340 340 help="""Specify what command to use to invoke a web
341 341 browser when opening the notebook. If not specified, the
342 342 default browser will be determined by the `webbrowser`
343 343 standard library module, which allows setting of the
344 344 BROWSER environment variable to override it.
345 345 """)
346 346
347 347 read_only = Bool(False, config=True,
348 348 help="Whether to prevent editing/execution of notebooks."
349 349 )
350 350
351 351 use_less = Bool(False, config=True,
352 352 help="""Wether to use Browser Side less-css parsing
353 353 instead of compiled css version in templates that allows
354 354 it. This is mainly convenient when working on the less
355 355 file to avoid a build step, or if user want to overwrite
356 356 some of the less variables without having to recompile
357 357 everything.
358 358
359 359 You will need to install the less.js component in the static directory
360 360 either in the source tree or in your profile folder.
361 361 """)
362 362
363 363 webapp_settings = Dict(config=True,
364 364 help="Supply overrides for the tornado.web.Application that the "
365 365 "IPython notebook uses.")
366 366
367 367 enable_mathjax = Bool(True, config=True,
368 368 help="""Whether to enable MathJax for typesetting math/TeX
369 369
370 370 MathJax is the javascript library IPython uses to render math/LaTeX. It is
371 371 very large, so you may want to disable it if you have a slow internet
372 372 connection, or for offline use of the notebook.
373 373
374 374 When disabled, equations etc. will appear as their untransformed TeX source.
375 375 """
376 376 )
377 377 def _enable_mathjax_changed(self, name, old, new):
378 378 """set mathjax url to empty if mathjax is disabled"""
379 379 if not new:
380 380 self.mathjax_url = u''
381 381
382 382 base_project_url = Unicode('/', config=True,
383 383 help='''The base URL for the notebook server.
384 384
385 385 Leading and trailing slashes can be omitted,
386 386 and will automatically be added.
387 387 ''')
388 388 def _base_project_url_changed(self, name, old, new):
389 389 if not new.startswith('/'):
390 390 self.base_project_url = '/'+new
391 391 elif not new.endswith('/'):
392 392 self.base_project_url = new+'/'
393 393
394 394 base_kernel_url = Unicode('/', config=True,
395 395 help='''The base URL for the kernel server
396 396
397 397 Leading and trailing slashes can be omitted,
398 398 and will automatically be added.
399 399 ''')
400 400 def _base_kernel_url_changed(self, name, old, new):
401 401 if not new.startswith('/'):
402 402 self.base_kernel_url = '/'+new
403 403 elif not new.endswith('/'):
404 404 self.base_kernel_url = new+'/'
405 405
406 406 websocket_host = Unicode("", config=True,
407 407 help="""The hostname for the websocket server."""
408 408 )
409 409
410 410 extra_static_paths = List(Unicode, config=True,
411 411 help="""Extra paths to search for serving static files.
412 412
413 413 This allows adding javascript/css to be available from the notebook server machine,
414 414 or overriding individual files in the IPython"""
415 415 )
416 416 def _extra_static_paths_default(self):
417 417 return [os.path.join(self.profile_dir.location, 'static')]
418 418
419 419 @property
420 420 def static_file_path(self):
421 421 """return extra paths + the default location"""
422 422 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
423 423
424 424 mathjax_url = Unicode("", config=True,
425 425 help="""The url for MathJax.js."""
426 426 )
427 427 def _mathjax_url_default(self):
428 428 if not self.enable_mathjax:
429 429 return u''
430 430 static_url_prefix = self.webapp_settings.get("static_url_prefix",
431 431 "/static/")
432 432 try:
433 433 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
434 434 except IOError:
435 435 if self.certfile:
436 436 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
437 437 base = u"https://c328740.ssl.cf1.rackcdn.com"
438 438 else:
439 439 base = u"http://cdn.mathjax.org"
440 440
441 441 url = base + u"/mathjax/latest/MathJax.js"
442 442 self.log.info("Using MathJax from CDN: %s", url)
443 443 return url
444 444 else:
445 445 self.log.info("Using local MathJax from %s" % mathjax)
446 446 return static_url_prefix+u"mathjax/MathJax.js"
447 447
448 448 def _mathjax_url_changed(self, name, old, new):
449 449 if new and not self.enable_mathjax:
450 450 # enable_mathjax=False overrides mathjax_url
451 451 self.mathjax_url = u''
452 452 else:
453 453 self.log.info("Using MathJax: %s", new)
454 454
455 455 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
456 456 config=True,
457 457 help='The notebook manager class to use.')
458 458
459 459 def parse_command_line(self, argv=None):
460 460 super(NotebookApp, self).parse_command_line(argv)
461 461 if argv is None:
462 462 argv = sys.argv[1:]
463 463
464 464 # Scrub frontend-specific flags
465 465 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
466 466 # Kernel should inherit default config file from frontend
467 467 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
468 468
469 469 if self.extra_args:
470 470 f = os.path.abspath(self.extra_args[0])
471 471 if os.path.isdir(f):
472 472 nbdir = f
473 473 else:
474 474 self.file_to_run = f
475 475 nbdir = os.path.dirname(f)
476 476 self.config.NotebookManager.notebook_dir = nbdir
477 477
478 478 def init_configurables(self):
479 479 # force Session default to be secure
480 480 default_secure(self.config)
481 481 self.kernel_manager = MappingKernelManager(
482 482 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
483 483 connection_dir = self.profile_dir.security_dir,
484 484 )
485 485 kls = import_item(self.notebook_manager_class)
486 486 self.notebook_manager = kls(config=self.config, log=self.log)
487 487 self.notebook_manager.load_notebook_names()
488 488 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
489 489 self.cluster_manager.update_profiles()
490 490
491 491 def init_logging(self):
492 492 # This prevents double log messages because tornado use a root logger that
493 493 # self.log is a child of. The logging module dipatches log messages to a log
494 494 # and all of its ancenstors until propagate is set to False.
495 495 self.log.propagate = False
496 496
497 497 def init_webapp(self):
498 498 """initialize tornado webapp and httpserver"""
499 499 self.web_app = NotebookWebApplication(
500 500 self, self.kernel_manager, self.notebook_manager,
501 501 self.cluster_manager, self.log,
502 502 self.base_project_url, self.webapp_settings
503 503 )
504 504 if self.certfile:
505 505 ssl_options = dict(certfile=self.certfile)
506 506 if self.keyfile:
507 507 ssl_options['keyfile'] = self.keyfile
508 508 else:
509 509 ssl_options = None
510 510 self.web_app.password = self.password
511 511 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
512 512 if not self.ip:
513 513 warning = "WARNING: The notebook server is listening on all IP addresses"
514 514 if ssl_options is None:
515 515 self.log.critical(warning + " and not using encryption. This"
516 516 "is not recommended.")
517 517 if not self.password and not self.read_only:
518 518 self.log.critical(warning + "and not using authentication."
519 519 "This is highly insecure and not recommended.")
520 520 success = None
521 521 for port in random_ports(self.port, self.port_retries+1):
522 522 try:
523 523 self.http_server.listen(port, self.ip)
524 524 except socket.error as e:
525 525 if e.errno != errno.EADDRINUSE:
526 526 raise
527 527 self.log.info('The port %i is already in use, trying another random port.' % port)
528 528 else:
529 529 self.port = port
530 530 success = True
531 531 break
532 532 if not success:
533 533 self.log.critical('ERROR: the notebook server could not be started because '
534 534 'no available port could be found.')
535 535 self.exit(1)
536 536
537 537 def init_signal(self):
538 538 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
539 539 # safely extract zmq version info:
540 540 try:
541 541 zmq_v = zmq.pyzmq_version_info()
542 542 except AttributeError:
543 543 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
544 544 if 'dev' in zmq.__version__:
545 545 zmq_v.append(999)
546 546 zmq_v = tuple(zmq_v)
547 547 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
548 548 # This won't work with 2.1.7 and
549 549 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
550 550 # but it will work
551 551 signal.signal(signal.SIGINT, self._handle_sigint)
552 552 signal.signal(signal.SIGTERM, self._signal_stop)
553 553 signal.signal(signal.SIGUSR1, self._signal_info)
554 554 if hasattr(signal, 'SIGINFO'):
555 555 # only on BSD-based systems
556 556 signal.signal(signal.SIGINFO, self._signal_info)
557 557
558 558 def _handle_sigint(self, sig, frame):
559 559 """SIGINT handler spawns confirmation dialog"""
560 560 # register more forceful signal handler for ^C^C case
561 561 signal.signal(signal.SIGINT, self._signal_stop)
562 562 # request confirmation dialog in bg thread, to avoid
563 563 # blocking the App
564 564 thread = threading.Thread(target=self._confirm_exit)
565 565 thread.daemon = True
566 566 thread.start()
567 567
568 568 def _restore_sigint_handler(self):
569 569 """callback for restoring original SIGINT handler"""
570 570 signal.signal(signal.SIGINT, self._handle_sigint)
571 571
572 572 def _confirm_exit(self):
573 573 """confirm shutdown on ^C
574 574
575 575 A second ^C, or answering 'y' within 5s will cause shutdown,
576 576 otherwise original SIGINT handler will be restored.
577 577
578 578 This doesn't work on Windows.
579 579 """
580 580 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
581 581 time.sleep(0.1)
582 582 info = self.log.info
583 583 info('interrupted')
584 self.print_notebook_info()
585 info("Shutdown this notebook server (y/[n])? ")
584 print self.notebook_info()
585 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
586 586 sys.stdout.flush()
587 587 r,w,x = select.select([sys.stdin], [], [], 5)
588 588 if r:
589 589 line = sys.stdin.readline()
590 590 if line.lower().startswith('y'):
591 591 self.log.critical("Shutdown confirmed")
592 592 ioloop.IOLoop.instance().stop()
593 593 return
594 594 else:
595 595 print "No answer for 5s:",
596 596 print "resuming operation..."
597 597 # no answer, or answer is no:
598 598 # set it back to original SIGINT handler
599 599 # use IOLoop.add_callback because signal.signal must be called
600 600 # from main thread
601 601 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
602 602
603 603 def _signal_stop(self, sig, frame):
604 604 self.log.critical("received signal %s, stopping", sig)
605 605 ioloop.IOLoop.instance().stop()
606 606
607 607 def _signal_info(self, sig, frame):
608 self.print_notebook_info()
608 print self.notebook_info()
609 609
610 610 @catch_config_error
611 611 def initialize(self, argv=None):
612 612 self.init_logging()
613 613 super(NotebookApp, self).initialize(argv)
614 614 self.init_configurables()
615 615 self.init_webapp()
616 616 self.init_signal()
617 617
618 618 def cleanup_kernels(self):
619 619 """Shutdown all kernels.
620 620
621 621 The kernels will shutdown themselves when this process no longer exists,
622 622 but explicit shutdown allows the KernelManagers to cleanup the connection files.
623 623 """
624 624 self.log.info('Shutting down kernels')
625 625 self.kernel_manager.shutdown_all()
626 626
627 def print_notebook_info(self):
628 "Print the current working directory and the server url information"
629 self.notebook_manager.log_info()
630 self.log.info("The IPython Notebook is running at: %s" % self._url)
627 def notebook_info(self):
628 "Return the current working directory and the server url information"
629 mgr_info = self.notebook_manager.info_string() + "\n"
630 return mgr_info +"The IPython Notebook is running at: %s" % self._url
631 631
632 632 def start(self):
633 633 """ Start the IPython Notebok server app, after initialization
634 634
635 635 This method takes no arguments so all configuration and initialization
636 636 must be done prior to calling this method."""
637 637 ip = self.ip if self.ip else '[all ip addresses on your system]'
638 638 proto = 'https' if self.certfile else 'http'
639 639 info = self.log.info
640 640 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
641 641 self.base_project_url)
642 self.print_notebook_info()
642 for line in self.notebook_info().split("\n"):
643 info(line)
643 644 info("Use Control-C to stop this server and shut down all kernels.")
644 645
645 646 if self.open_browser or self.file_to_run:
646 647 ip = self.ip or LOCALHOST
647 648 try:
648 649 browser = webbrowser.get(self.browser or None)
649 650 except webbrowser.Error as e:
650 651 self.log.warn('No web browser found: %s.' % e)
651 652 browser = None
652 653
653 654 if self.file_to_run:
654 655 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
655 656 url = self.notebook_manager.rev_mapping.get(name, '')
656 657 else:
657 658 url = ''
658 659 if browser:
659 660 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
660 661 self.port, self.base_project_url, url), new=2)
661 662 threading.Thread(target=b).start()
662 663 try:
663 664 ioloop.IOLoop.instance().start()
664 665 except KeyboardInterrupt:
665 666 info("Interrupted...")
666 667 finally:
667 668 self.cleanup_kernels()
668 669
669 670
670 671 #-----------------------------------------------------------------------------
671 672 # Main entry point
672 673 #-----------------------------------------------------------------------------
673 674
674 675 def launch_new_instance():
675 676 app = NotebookApp.instance()
676 677 app.initialize()
677 678 app.start()
678 679
General Comments 0
You need to be logged in to leave comments. Login now