##// END OF EJS Templates
Merge pull request #1652 from minrk/zmqcompat...
Fernando Perez -
r6632:64a50619 merge
parent child Browse files
Show More
@@ -1,15 +1,20 b''
1 """The IPython HTML Notebook"""
1 """The IPython HTML Notebook"""
2
2
3 # check for tornado 2.1.0
3 # check for tornado 2.1.0
4 msg = "The IPython Notebook requires tornado >= 2.1.0"
4 msg = "The IPython Notebook requires tornado >= 2.1.0"
5 try:
5 try:
6 import tornado
6 import tornado
7 except ImportError:
7 except ImportError:
8 raise ImportError(msg)
8 raise ImportError(msg)
9 try:
9 try:
10 version_info = tornado.version_info
10 version_info = tornado.version_info
11 except AttributeError:
11 except AttributeError:
12 raise ImportError(msg + ", but you have < 1.1.0")
12 raise ImportError(msg + ", but you have < 1.1.0")
13 if version_info < (2,1,0):
13 if version_info < (2,1,0):
14 raise ImportError(msg + ", but you have %s" % tornado.version)
14 raise ImportError(msg + ", but you have %s" % tornado.version)
15 del msg
15 del msg
16
17 # check for pyzmq 2.1.4
18 from IPython.zmq import check_for_zmq
19 check_for_zmq('2.1.4', 'IPython.frontend.html.notebook')
20 del check_for_zmq
@@ -1,569 +1,563 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import re
23 import re
24 import select
24 import select
25 import signal
25 import signal
26 import socket
26 import socket
27 import sys
27 import sys
28 import threading
28 import threading
29 import time
29 import time
30 import webbrowser
30 import webbrowser
31
31
32 # Third party
32 # Third party
33 import zmq
33 import zmq
34
34
35 # Install the pyzmq ioloop. This has to be done before anything else from
35 # Install the pyzmq ioloop. This has to be done before anything else from
36 # tornado is imported.
36 # tornado is imported.
37 from zmq.eventloop import ioloop
37 from zmq.eventloop import ioloop
38 # FIXME: ioloop.install is new in pyzmq-2.1.7, so remove this conditional
38 ioloop.install()
39 # when pyzmq dependency is updated beyond that.
40 if hasattr(ioloop, 'install'):
41 ioloop.install()
42 else:
43 import tornado.ioloop
44 tornado.ioloop.IOLoop = ioloop.IOLoop
45
39
46 from tornado import httpserver
40 from tornado import httpserver
47 from tornado import web
41 from tornado import web
48
42
49 # Our own libraries
43 # Our own libraries
50 from .kernelmanager import MappingKernelManager
44 from .kernelmanager import MappingKernelManager
51 from .handlers import (LoginHandler, LogoutHandler,
45 from .handlers import (LoginHandler, LogoutHandler,
52 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
46 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
53 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
47 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
54 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
48 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
55 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
49 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
56 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
50 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
57 )
51 )
58 from .notebookmanager import NotebookManager
52 from .notebookmanager import NotebookManager
59 from .clustermanager import ClusterManager
53 from .clustermanager import ClusterManager
60
54
61 from IPython.config.application import catch_config_error, boolean_flag
55 from IPython.config.application import catch_config_error, boolean_flag
62 from IPython.core.application import BaseIPythonApplication
56 from IPython.core.application import BaseIPythonApplication
63 from IPython.core.profiledir import ProfileDir
57 from IPython.core.profiledir import ProfileDir
64 from IPython.lib.kernel import swallow_argv
58 from IPython.lib.kernel import swallow_argv
65 from IPython.zmq.session import Session, default_secure
59 from IPython.zmq.session import Session, default_secure
66 from IPython.zmq.zmqshell import ZMQInteractiveShell
60 from IPython.zmq.zmqshell import ZMQInteractiveShell
67 from IPython.zmq.ipkernel import (
61 from IPython.zmq.ipkernel import (
68 flags as ipkernel_flags,
62 flags as ipkernel_flags,
69 aliases as ipkernel_aliases,
63 aliases as ipkernel_aliases,
70 IPKernelApp
64 IPKernelApp
71 )
65 )
72 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
66 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
73 from IPython.utils import py3compat
67 from IPython.utils import py3compat
74
68
75 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
76 # Module globals
70 # Module globals
77 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
78
72
79 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
73 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
80 _kernel_action_regex = r"(?P<action>restart|interrupt)"
74 _kernel_action_regex = r"(?P<action>restart|interrupt)"
81 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
75 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
82 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
76 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
83 _cluster_action_regex = r"(?P<action>start|stop)"
77 _cluster_action_regex = r"(?P<action>start|stop)"
84
78
85
79
86 LOCALHOST = '127.0.0.1'
80 LOCALHOST = '127.0.0.1'
87
81
88 _examples = """
82 _examples = """
89 ipython notebook # start the notebook
83 ipython notebook # start the notebook
90 ipython notebook --profile=sympy # use the sympy profile
84 ipython notebook --profile=sympy # use the sympy profile
91 ipython notebook --pylab=inline # pylab in inline plotting mode
85 ipython notebook --pylab=inline # pylab in inline plotting mode
92 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
86 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
93 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
87 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
94 """
88 """
95
89
96 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
97 # Helper functions
91 # Helper functions
98 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
99
93
100 def url_path_join(a,b):
94 def url_path_join(a,b):
101 if a.endswith('/') and b.startswith('/'):
95 if a.endswith('/') and b.startswith('/'):
102 return a[:-1]+b
96 return a[:-1]+b
103 else:
97 else:
104 return a+b
98 return a+b
105
99
106 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
107 # The Tornado web application
101 # The Tornado web application
108 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
109
103
110 class NotebookWebApplication(web.Application):
104 class NotebookWebApplication(web.Application):
111
105
112 def __init__(self, ipython_app, kernel_manager, notebook_manager,
106 def __init__(self, ipython_app, kernel_manager, notebook_manager,
113 cluster_manager, log,
107 cluster_manager, log,
114 base_project_url, settings_overrides):
108 base_project_url, settings_overrides):
115 handlers = [
109 handlers = [
116 (r"/", ProjectDashboardHandler),
110 (r"/", ProjectDashboardHandler),
117 (r"/login", LoginHandler),
111 (r"/login", LoginHandler),
118 (r"/logout", LogoutHandler),
112 (r"/logout", LogoutHandler),
119 (r"/new", NewHandler),
113 (r"/new", NewHandler),
120 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
114 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
121 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
115 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
122 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
116 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
123 (r"/kernels", MainKernelHandler),
117 (r"/kernels", MainKernelHandler),
124 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
118 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
125 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
119 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
126 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
120 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
127 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
121 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
128 (r"/notebooks", NotebookRootHandler),
122 (r"/notebooks", NotebookRootHandler),
129 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
123 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
130 (r"/rstservice/render", RSTHandler),
124 (r"/rstservice/render", RSTHandler),
131 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
125 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
132 (r"/clusters", MainClusterHandler),
126 (r"/clusters", MainClusterHandler),
133 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
127 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
134 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
128 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
135 ]
129 ]
136 settings = dict(
130 settings = dict(
137 template_path=os.path.join(os.path.dirname(__file__), "templates"),
131 template_path=os.path.join(os.path.dirname(__file__), "templates"),
138 static_path=os.path.join(os.path.dirname(__file__), "static"),
132 static_path=os.path.join(os.path.dirname(__file__), "static"),
139 cookie_secret=os.urandom(1024),
133 cookie_secret=os.urandom(1024),
140 login_url="/login",
134 login_url="/login",
141 )
135 )
142
136
143 # allow custom overrides for the tornado web app.
137 # allow custom overrides for the tornado web app.
144 settings.update(settings_overrides)
138 settings.update(settings_overrides)
145
139
146 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
147 # base_project_url will always be unicode, which will in turn
141 # base_project_url will always be unicode, which will in turn
148 # make the patterns unicode, and ultimately result in unicode
142 # make the patterns unicode, and ultimately result in unicode
149 # keys in kwargs to handler._execute(**kwargs) in tornado.
143 # keys in kwargs to handler._execute(**kwargs) in tornado.
150 # This enforces that base_project_url be ascii in that situation.
144 # This enforces that base_project_url be ascii in that situation.
151 #
145 #
152 # Note that the URLs these patterns check against are escaped,
146 # Note that the URLs these patterns check against are escaped,
153 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
147 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
154 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
148 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
155
149
156 # prepend base_project_url onto the patterns that we match
150 # prepend base_project_url onto the patterns that we match
157 new_handlers = []
151 new_handlers = []
158 for handler in handlers:
152 for handler in handlers:
159 pattern = url_path_join(base_project_url, handler[0])
153 pattern = url_path_join(base_project_url, handler[0])
160 new_handler = tuple([pattern]+list(handler[1:]))
154 new_handler = tuple([pattern]+list(handler[1:]))
161 new_handlers.append( new_handler )
155 new_handlers.append( new_handler )
162
156
163 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
157 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
164
158
165 self.kernel_manager = kernel_manager
159 self.kernel_manager = kernel_manager
166 self.notebook_manager = notebook_manager
160 self.notebook_manager = notebook_manager
167 self.cluster_manager = cluster_manager
161 self.cluster_manager = cluster_manager
168 self.ipython_app = ipython_app
162 self.ipython_app = ipython_app
169 self.read_only = self.ipython_app.read_only
163 self.read_only = self.ipython_app.read_only
170 self.log = log
164 self.log = log
171
165
172
166
173 #-----------------------------------------------------------------------------
167 #-----------------------------------------------------------------------------
174 # Aliases and Flags
168 # Aliases and Flags
175 #-----------------------------------------------------------------------------
169 #-----------------------------------------------------------------------------
176
170
177 flags = dict(ipkernel_flags)
171 flags = dict(ipkernel_flags)
178 flags['no-browser']=(
172 flags['no-browser']=(
179 {'NotebookApp' : {'open_browser' : False}},
173 {'NotebookApp' : {'open_browser' : False}},
180 "Don't open the notebook in a browser after startup."
174 "Don't open the notebook in a browser after startup."
181 )
175 )
182 flags['no-mathjax']=(
176 flags['no-mathjax']=(
183 {'NotebookApp' : {'enable_mathjax' : False}},
177 {'NotebookApp' : {'enable_mathjax' : False}},
184 """Disable MathJax
178 """Disable MathJax
185
179
186 MathJax is the javascript library IPython uses to render math/LaTeX. It is
180 MathJax is the javascript library IPython uses to render math/LaTeX. It is
187 very large, so you may want to disable it if you have a slow internet
181 very large, so you may want to disable it if you have a slow internet
188 connection, or for offline use of the notebook.
182 connection, or for offline use of the notebook.
189
183
190 When disabled, equations etc. will appear as their untransformed TeX source.
184 When disabled, equations etc. will appear as their untransformed TeX source.
191 """
185 """
192 )
186 )
193 flags['read-only'] = (
187 flags['read-only'] = (
194 {'NotebookApp' : {'read_only' : True}},
188 {'NotebookApp' : {'read_only' : True}},
195 """Allow read-only access to notebooks.
189 """Allow read-only access to notebooks.
196
190
197 When using a password to protect the notebook server, this flag
191 When using a password to protect the notebook server, this flag
198 allows unauthenticated clients to view the notebook list, and
192 allows unauthenticated clients to view the notebook list, and
199 individual notebooks, but not edit them, start kernels, or run
193 individual notebooks, but not edit them, start kernels, or run
200 code.
194 code.
201
195
202 If no password is set, the server will be entirely read-only.
196 If no password is set, the server will be entirely read-only.
203 """
197 """
204 )
198 )
205
199
206 # Add notebook manager flags
200 # Add notebook manager flags
207 flags.update(boolean_flag('script', 'NotebookManager.save_script',
201 flags.update(boolean_flag('script', 'NotebookManager.save_script',
208 'Auto-save a .py script everytime the .ipynb notebook is saved',
202 'Auto-save a .py script everytime the .ipynb notebook is saved',
209 'Do not auto-save .py scripts for every notebook'))
203 'Do not auto-save .py scripts for every notebook'))
210
204
211 # the flags that are specific to the frontend
205 # the flags that are specific to the frontend
212 # these must be scrubbed before being passed to the kernel,
206 # these must be scrubbed before being passed to the kernel,
213 # or it will raise an error on unrecognized flags
207 # or it will raise an error on unrecognized flags
214 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
208 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
215
209
216 aliases = dict(ipkernel_aliases)
210 aliases = dict(ipkernel_aliases)
217
211
218 aliases.update({
212 aliases.update({
219 'ip': 'NotebookApp.ip',
213 'ip': 'NotebookApp.ip',
220 'port': 'NotebookApp.port',
214 'port': 'NotebookApp.port',
221 'keyfile': 'NotebookApp.keyfile',
215 'keyfile': 'NotebookApp.keyfile',
222 'certfile': 'NotebookApp.certfile',
216 'certfile': 'NotebookApp.certfile',
223 'notebook-dir': 'NotebookManager.notebook_dir',
217 'notebook-dir': 'NotebookManager.notebook_dir',
224 'browser': 'NotebookApp.browser',
218 'browser': 'NotebookApp.browser',
225 })
219 })
226
220
227 # remove ipkernel flags that are singletons, and don't make sense in
221 # remove ipkernel flags that are singletons, and don't make sense in
228 # multi-kernel evironment:
222 # multi-kernel evironment:
229 aliases.pop('f', None)
223 aliases.pop('f', None)
230
224
231 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
225 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
232 u'notebook-dir']
226 u'notebook-dir']
233
227
234 #-----------------------------------------------------------------------------
228 #-----------------------------------------------------------------------------
235 # NotebookApp
229 # NotebookApp
236 #-----------------------------------------------------------------------------
230 #-----------------------------------------------------------------------------
237
231
238 class NotebookApp(BaseIPythonApplication):
232 class NotebookApp(BaseIPythonApplication):
239
233
240 name = 'ipython-notebook'
234 name = 'ipython-notebook'
241 default_config_file_name='ipython_notebook_config.py'
235 default_config_file_name='ipython_notebook_config.py'
242
236
243 description = """
237 description = """
244 The IPython HTML Notebook.
238 The IPython HTML Notebook.
245
239
246 This launches a Tornado based HTML Notebook Server that serves up an
240 This launches a Tornado based HTML Notebook Server that serves up an
247 HTML5/Javascript Notebook client.
241 HTML5/Javascript Notebook client.
248 """
242 """
249 examples = _examples
243 examples = _examples
250
244
251 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
245 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
252 MappingKernelManager, NotebookManager]
246 MappingKernelManager, NotebookManager]
253 flags = Dict(flags)
247 flags = Dict(flags)
254 aliases = Dict(aliases)
248 aliases = Dict(aliases)
255
249
256 kernel_argv = List(Unicode)
250 kernel_argv = List(Unicode)
257
251
258 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
252 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
259 default_value=logging.INFO,
253 default_value=logging.INFO,
260 config=True,
254 config=True,
261 help="Set the log level by value or name.")
255 help="Set the log level by value or name.")
262
256
263 # create requested profiles by default, if they don't exist:
257 # create requested profiles by default, if they don't exist:
264 auto_create = Bool(True)
258 auto_create = Bool(True)
265
259
266 # Network related information.
260 # Network related information.
267
261
268 ip = Unicode(LOCALHOST, config=True,
262 ip = Unicode(LOCALHOST, config=True,
269 help="The IP address the notebook server will listen on."
263 help="The IP address the notebook server will listen on."
270 )
264 )
271
265
272 def _ip_changed(self, name, old, new):
266 def _ip_changed(self, name, old, new):
273 if new == u'*': self.ip = u''
267 if new == u'*': self.ip = u''
274
268
275 port = Integer(8888, config=True,
269 port = Integer(8888, config=True,
276 help="The port the notebook server will listen on."
270 help="The port the notebook server will listen on."
277 )
271 )
278
272
279 certfile = Unicode(u'', config=True,
273 certfile = Unicode(u'', config=True,
280 help="""The full path to an SSL/TLS certificate file."""
274 help="""The full path to an SSL/TLS certificate file."""
281 )
275 )
282
276
283 keyfile = Unicode(u'', config=True,
277 keyfile = Unicode(u'', config=True,
284 help="""The full path to a private key file for usage with SSL/TLS."""
278 help="""The full path to a private key file for usage with SSL/TLS."""
285 )
279 )
286
280
287 password = Unicode(u'', config=True,
281 password = Unicode(u'', config=True,
288 help="""Hashed password to use for web authentication.
282 help="""Hashed password to use for web authentication.
289
283
290 To generate, type in a python/IPython shell:
284 To generate, type in a python/IPython shell:
291
285
292 from IPython.lib import passwd; passwd()
286 from IPython.lib import passwd; passwd()
293
287
294 The string should be of the form type:salt:hashed-password.
288 The string should be of the form type:salt:hashed-password.
295 """
289 """
296 )
290 )
297
291
298 open_browser = Bool(True, config=True,
292 open_browser = Bool(True, config=True,
299 help="""Whether to open in a browser after starting.
293 help="""Whether to open in a browser after starting.
300 The specific browser used is platform dependent and
294 The specific browser used is platform dependent and
301 determined by the python standard library `webbrowser`
295 determined by the python standard library `webbrowser`
302 module, unless it is overridden using the --browser
296 module, unless it is overridden using the --browser
303 (NotebookApp.browser) configuration option.
297 (NotebookApp.browser) configuration option.
304 """)
298 """)
305
299
306 browser = Unicode(u'', config=True,
300 browser = Unicode(u'', config=True,
307 help="""Specify what command to use to invoke a web
301 help="""Specify what command to use to invoke a web
308 browser when opening the notebook. If not specified, the
302 browser when opening the notebook. If not specified, the
309 default browser will be determined by the `webbrowser`
303 default browser will be determined by the `webbrowser`
310 standard library module, which allows setting of the
304 standard library module, which allows setting of the
311 BROWSER environment variable to override it.
305 BROWSER environment variable to override it.
312 """)
306 """)
313
307
314 read_only = Bool(False, config=True,
308 read_only = Bool(False, config=True,
315 help="Whether to prevent editing/execution of notebooks."
309 help="Whether to prevent editing/execution of notebooks."
316 )
310 )
317
311
318 webapp_settings = Dict(config=True,
312 webapp_settings = Dict(config=True,
319 help="Supply overrides for the tornado.web.Application that the "
313 help="Supply overrides for the tornado.web.Application that the "
320 "IPython notebook uses.")
314 "IPython notebook uses.")
321
315
322 enable_mathjax = Bool(True, config=True,
316 enable_mathjax = Bool(True, config=True,
323 help="""Whether to enable MathJax for typesetting math/TeX
317 help="""Whether to enable MathJax for typesetting math/TeX
324
318
325 MathJax is the javascript library IPython uses to render math/LaTeX. It is
319 MathJax is the javascript library IPython uses to render math/LaTeX. It is
326 very large, so you may want to disable it if you have a slow internet
320 very large, so you may want to disable it if you have a slow internet
327 connection, or for offline use of the notebook.
321 connection, or for offline use of the notebook.
328
322
329 When disabled, equations etc. will appear as their untransformed TeX source.
323 When disabled, equations etc. will appear as their untransformed TeX source.
330 """
324 """
331 )
325 )
332 def _enable_mathjax_changed(self, name, old, new):
326 def _enable_mathjax_changed(self, name, old, new):
333 """set mathjax url to empty if mathjax is disabled"""
327 """set mathjax url to empty if mathjax is disabled"""
334 if not new:
328 if not new:
335 self.mathjax_url = u''
329 self.mathjax_url = u''
336
330
337 base_project_url = Unicode('/', config=True,
331 base_project_url = Unicode('/', config=True,
338 help='''The base URL for the notebook server''')
332 help='''The base URL for the notebook server''')
339 base_kernel_url = Unicode('/', config=True,
333 base_kernel_url = Unicode('/', config=True,
340 help='''The base URL for the kernel server''')
334 help='''The base URL for the kernel server''')
341 websocket_host = Unicode("", config=True,
335 websocket_host = Unicode("", config=True,
342 help="""The hostname for the websocket server."""
336 help="""The hostname for the websocket server."""
343 )
337 )
344
338
345 mathjax_url = Unicode("", config=True,
339 mathjax_url = Unicode("", config=True,
346 help="""The url for MathJax.js."""
340 help="""The url for MathJax.js."""
347 )
341 )
348 def _mathjax_url_default(self):
342 def _mathjax_url_default(self):
349 if not self.enable_mathjax:
343 if not self.enable_mathjax:
350 return u''
344 return u''
351 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
345 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
352 static_url_prefix = self.webapp_settings.get("static_url_prefix",
346 static_url_prefix = self.webapp_settings.get("static_url_prefix",
353 "/static/")
347 "/static/")
354 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
348 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
355 self.log.info("Using local MathJax")
349 self.log.info("Using local MathJax")
356 return static_url_prefix+u"mathjax/MathJax.js"
350 return static_url_prefix+u"mathjax/MathJax.js"
357 else:
351 else:
358 self.log.info("Using MathJax from CDN")
352 self.log.info("Using MathJax from CDN")
359 hostname = "cdn.mathjax.org"
353 hostname = "cdn.mathjax.org"
360 try:
354 try:
361 # resolve mathjax cdn alias to cloudfront, because Amazon's SSL certificate
355 # resolve mathjax cdn alias to cloudfront, because Amazon's SSL certificate
362 # only works on *.cloudfront.net
356 # only works on *.cloudfront.net
363 true_host, aliases, IPs = socket.gethostbyname_ex(hostname)
357 true_host, aliases, IPs = socket.gethostbyname_ex(hostname)
364 # I've run this on a few machines, and some put the right answer in true_host,
358 # I've run this on a few machines, and some put the right answer in true_host,
365 # while others gave it in the aliases list, so we check both.
359 # while others gave it in the aliases list, so we check both.
366 aliases.insert(0, true_host)
360 aliases.insert(0, true_host)
367 except Exception:
361 except Exception:
368 self.log.warn("Couldn't determine MathJax CDN info")
362 self.log.warn("Couldn't determine MathJax CDN info")
369 else:
363 else:
370 for alias in aliases:
364 for alias in aliases:
371 parts = alias.split('.')
365 parts = alias.split('.')
372 # want static foo.cloudfront.net, not dynamic foo.lax3.cloudfront.net
366 # want static foo.cloudfront.net, not dynamic foo.lax3.cloudfront.net
373 if len(parts) == 3 and alias.endswith(".cloudfront.net"):
367 if len(parts) == 3 and alias.endswith(".cloudfront.net"):
374 hostname = alias
368 hostname = alias
375 break
369 break
376
370
377 if not hostname.endswith(".cloudfront.net"):
371 if not hostname.endswith(".cloudfront.net"):
378 self.log.error("Couldn't resolve CloudFront host, required for HTTPS MathJax.")
372 self.log.error("Couldn't resolve CloudFront host, required for HTTPS MathJax.")
379 self.log.error("Loading from https://cdn.mathjax.org will probably fail due to invalid certificate.")
373 self.log.error("Loading from https://cdn.mathjax.org will probably fail due to invalid certificate.")
380 self.log.error("For unsecured HTTP access to MathJax use config:")
374 self.log.error("For unsecured HTTP access to MathJax use config:")
381 self.log.error("NotebookApp.mathjax_url='http://cdn.mathjax.org/mathjax/latest/MathJax.js'")
375 self.log.error("NotebookApp.mathjax_url='http://cdn.mathjax.org/mathjax/latest/MathJax.js'")
382 return u"https://%s/mathjax/latest/MathJax.js" % hostname
376 return u"https://%s/mathjax/latest/MathJax.js" % hostname
383
377
384 def _mathjax_url_changed(self, name, old, new):
378 def _mathjax_url_changed(self, name, old, new):
385 if new and not self.enable_mathjax:
379 if new and not self.enable_mathjax:
386 # enable_mathjax=False overrides mathjax_url
380 # enable_mathjax=False overrides mathjax_url
387 self.mathjax_url = u''
381 self.mathjax_url = u''
388 else:
382 else:
389 self.log.info("Using MathJax: %s", new)
383 self.log.info("Using MathJax: %s", new)
390
384
391 def parse_command_line(self, argv=None):
385 def parse_command_line(self, argv=None):
392 super(NotebookApp, self).parse_command_line(argv)
386 super(NotebookApp, self).parse_command_line(argv)
393 if argv is None:
387 if argv is None:
394 argv = sys.argv[1:]
388 argv = sys.argv[1:]
395
389
396 # Scrub frontend-specific flags
390 # Scrub frontend-specific flags
397 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
391 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
398 # Kernel should inherit default config file from frontend
392 # Kernel should inherit default config file from frontend
399 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
393 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
400
394
401 def init_configurables(self):
395 def init_configurables(self):
402 # force Session default to be secure
396 # force Session default to be secure
403 default_secure(self.config)
397 default_secure(self.config)
404 # Create a KernelManager and start a kernel.
398 # Create a KernelManager and start a kernel.
405 self.kernel_manager = MappingKernelManager(
399 self.kernel_manager = MappingKernelManager(
406 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
400 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
407 connection_dir = self.profile_dir.security_dir,
401 connection_dir = self.profile_dir.security_dir,
408 )
402 )
409 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
403 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
410 self.notebook_manager.list_notebooks()
404 self.notebook_manager.list_notebooks()
411 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
405 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
412 self.cluster_manager.update_profiles()
406 self.cluster_manager.update_profiles()
413
407
414 def init_logging(self):
408 def init_logging(self):
415 super(NotebookApp, self).init_logging()
409 super(NotebookApp, self).init_logging()
416 # This prevents double log messages because tornado use a root logger that
410 # This prevents double log messages because tornado use a root logger that
417 # self.log is a child of. The logging module dipatches log messages to a log
411 # self.log is a child of. The logging module dipatches log messages to a log
418 # and all of its ancenstors until propagate is set to False.
412 # and all of its ancenstors until propagate is set to False.
419 self.log.propagate = False
413 self.log.propagate = False
420
414
421 def init_webapp(self):
415 def init_webapp(self):
422 """initialize tornado webapp and httpserver"""
416 """initialize tornado webapp and httpserver"""
423 self.web_app = NotebookWebApplication(
417 self.web_app = NotebookWebApplication(
424 self, self.kernel_manager, self.notebook_manager,
418 self, self.kernel_manager, self.notebook_manager,
425 self.cluster_manager, self.log,
419 self.cluster_manager, self.log,
426 self.base_project_url, self.webapp_settings
420 self.base_project_url, self.webapp_settings
427 )
421 )
428 if self.certfile:
422 if self.certfile:
429 ssl_options = dict(certfile=self.certfile)
423 ssl_options = dict(certfile=self.certfile)
430 if self.keyfile:
424 if self.keyfile:
431 ssl_options['keyfile'] = self.keyfile
425 ssl_options['keyfile'] = self.keyfile
432 else:
426 else:
433 ssl_options = None
427 ssl_options = None
434 self.web_app.password = self.password
428 self.web_app.password = self.password
435 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
429 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
436 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
430 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
437 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
431 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
438 'but not using any encryption or authentication. This is highly '
432 'but not using any encryption or authentication. This is highly '
439 'insecure and not recommended.')
433 'insecure and not recommended.')
440
434
441 # Try random ports centered around the default.
435 # Try random ports centered around the default.
442 from random import randint
436 from random import randint
443 n = 50 # Max number of attempts, keep reasonably large.
437 n = 50 # Max number of attempts, keep reasonably large.
444 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
438 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
445 try:
439 try:
446 self.http_server.listen(port, self.ip)
440 self.http_server.listen(port, self.ip)
447 except socket.error, e:
441 except socket.error, e:
448 if e.errno != errno.EADDRINUSE:
442 if e.errno != errno.EADDRINUSE:
449 raise
443 raise
450 self.log.info('The port %i is already in use, trying another random port.' % port)
444 self.log.info('The port %i is already in use, trying another random port.' % port)
451 else:
445 else:
452 self.port = port
446 self.port = port
453 break
447 break
454
448
455 def init_signal(self):
449 def init_signal(self):
456 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
450 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
457 # safely extract zmq version info:
451 # safely extract zmq version info:
458 try:
452 try:
459 zmq_v = zmq.pyzmq_version_info()
453 zmq_v = zmq.pyzmq_version_info()
460 except AttributeError:
454 except AttributeError:
461 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
455 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
462 if 'dev' in zmq.__version__:
456 if 'dev' in zmq.__version__:
463 zmq_v.append(999)
457 zmq_v.append(999)
464 zmq_v = tuple(zmq_v)
458 zmq_v = tuple(zmq_v)
465 if zmq_v >= (2,1,9):
459 if zmq_v >= (2,1,9):
466 # This won't work with 2.1.7 and
460 # This won't work with 2.1.7 and
467 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
461 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
468 # but it will work
462 # but it will work
469 signal.signal(signal.SIGINT, self._handle_sigint)
463 signal.signal(signal.SIGINT, self._handle_sigint)
470 signal.signal(signal.SIGTERM, self._signal_stop)
464 signal.signal(signal.SIGTERM, self._signal_stop)
471
465
472 def _handle_sigint(self, sig, frame):
466 def _handle_sigint(self, sig, frame):
473 """SIGINT handler spawns confirmation dialog"""
467 """SIGINT handler spawns confirmation dialog"""
474 # register more forceful signal handler for ^C^C case
468 # register more forceful signal handler for ^C^C case
475 signal.signal(signal.SIGINT, self._signal_stop)
469 signal.signal(signal.SIGINT, self._signal_stop)
476 # request confirmation dialog in bg thread, to avoid
470 # request confirmation dialog in bg thread, to avoid
477 # blocking the App
471 # blocking the App
478 thread = threading.Thread(target=self._confirm_exit)
472 thread = threading.Thread(target=self._confirm_exit)
479 thread.daemon = True
473 thread.daemon = True
480 thread.start()
474 thread.start()
481
475
482 def _restore_sigint_handler(self):
476 def _restore_sigint_handler(self):
483 """callback for restoring original SIGINT handler"""
477 """callback for restoring original SIGINT handler"""
484 signal.signal(signal.SIGINT, self._handle_sigint)
478 signal.signal(signal.SIGINT, self._handle_sigint)
485
479
486 def _confirm_exit(self):
480 def _confirm_exit(self):
487 """confirm shutdown on ^C
481 """confirm shutdown on ^C
488
482
489 A second ^C, or answering 'y' within 5s will cause shutdown,
483 A second ^C, or answering 'y' within 5s will cause shutdown,
490 otherwise original SIGINT handler will be restored.
484 otherwise original SIGINT handler will be restored.
491 """
485 """
492 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
486 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
493 time.sleep(0.1)
487 time.sleep(0.1)
494 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
488 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
495 sys.stdout.flush()
489 sys.stdout.flush()
496 r,w,x = select.select([sys.stdin], [], [], 5)
490 r,w,x = select.select([sys.stdin], [], [], 5)
497 if r:
491 if r:
498 line = sys.stdin.readline()
492 line = sys.stdin.readline()
499 if line.lower().startswith('y'):
493 if line.lower().startswith('y'):
500 self.log.critical("Shutdown confirmed")
494 self.log.critical("Shutdown confirmed")
501 ioloop.IOLoop.instance().stop()
495 ioloop.IOLoop.instance().stop()
502 return
496 return
503 else:
497 else:
504 print "No answer for 5s:",
498 print "No answer for 5s:",
505 print "resuming operation..."
499 print "resuming operation..."
506 # no answer, or answer is no:
500 # no answer, or answer is no:
507 # set it back to original SIGINT handler
501 # set it back to original SIGINT handler
508 # use IOLoop.add_callback because signal.signal must be called
502 # use IOLoop.add_callback because signal.signal must be called
509 # from main thread
503 # from main thread
510 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
504 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
511
505
512 def _signal_stop(self, sig, frame):
506 def _signal_stop(self, sig, frame):
513 self.log.critical("received signal %s, stopping", sig)
507 self.log.critical("received signal %s, stopping", sig)
514 ioloop.IOLoop.instance().stop()
508 ioloop.IOLoop.instance().stop()
515
509
516 @catch_config_error
510 @catch_config_error
517 def initialize(self, argv=None):
511 def initialize(self, argv=None):
518 super(NotebookApp, self).initialize(argv)
512 super(NotebookApp, self).initialize(argv)
519 self.init_configurables()
513 self.init_configurables()
520 self.init_webapp()
514 self.init_webapp()
521 self.init_signal()
515 self.init_signal()
522
516
523 def cleanup_kernels(self):
517 def cleanup_kernels(self):
524 """shutdown all kernels
518 """shutdown all kernels
525
519
526 The kernels will shutdown themselves when this process no longer exists,
520 The kernels will shutdown themselves when this process no longer exists,
527 but explicit shutdown allows the KernelManagers to cleanup the connection files.
521 but explicit shutdown allows the KernelManagers to cleanup the connection files.
528 """
522 """
529 self.log.info('Shutting down kernels')
523 self.log.info('Shutting down kernels')
530 km = self.kernel_manager
524 km = self.kernel_manager
531 # copy list, since kill_kernel deletes keys
525 # copy list, since kill_kernel deletes keys
532 for kid in list(km.kernel_ids):
526 for kid in list(km.kernel_ids):
533 km.kill_kernel(kid)
527 km.kill_kernel(kid)
534
528
535 def start(self):
529 def start(self):
536 ip = self.ip if self.ip else '[all ip addresses on your system]'
530 ip = self.ip if self.ip else '[all ip addresses on your system]'
537 proto = 'https' if self.certfile else 'http'
531 proto = 'https' if self.certfile else 'http'
538 info = self.log.info
532 info = self.log.info
539 info("The IPython Notebook is running at: %s://%s:%i%s" %
533 info("The IPython Notebook is running at: %s://%s:%i%s" %
540 (proto, ip, self.port,self.base_project_url) )
534 (proto, ip, self.port,self.base_project_url) )
541 info("Use Control-C to stop this server and shut down all kernels.")
535 info("Use Control-C to stop this server and shut down all kernels.")
542
536
543 if self.open_browser:
537 if self.open_browser:
544 ip = self.ip or '127.0.0.1'
538 ip = self.ip or '127.0.0.1'
545 if self.browser:
539 if self.browser:
546 browser = webbrowser.get(self.browser)
540 browser = webbrowser.get(self.browser)
547 else:
541 else:
548 browser = webbrowser.get()
542 browser = webbrowser.get()
549 b = lambda : browser.open("%s://%s:%i%s" % (proto, ip, self.port,
543 b = lambda : browser.open("%s://%s:%i%s" % (proto, ip, self.port,
550 self.base_project_url),
544 self.base_project_url),
551 new=2)
545 new=2)
552 threading.Thread(target=b).start()
546 threading.Thread(target=b).start()
553 try:
547 try:
554 ioloop.IOLoop.instance().start()
548 ioloop.IOLoop.instance().start()
555 except KeyboardInterrupt:
549 except KeyboardInterrupt:
556 info("Interrupted...")
550 info("Interrupted...")
557 finally:
551 finally:
558 self.cleanup_kernels()
552 self.cleanup_kernels()
559
553
560
554
561 #-----------------------------------------------------------------------------
555 #-----------------------------------------------------------------------------
562 # Main entry point
556 # Main entry point
563 #-----------------------------------------------------------------------------
557 #-----------------------------------------------------------------------------
564
558
565 def launch_new_instance():
559 def launch_new_instance():
566 app = NotebookApp.instance()
560 app = NotebookApp.instance()
567 app.initialize()
561 app.initialize()
568 app.start()
562 app.start()
569
563
@@ -1,39 +1,68 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (C) 2010-2011 The IPython Development Team
2 # Copyright (C) 2010-2011 The IPython Development Team
3 #
3 #
4 # Distributed under the terms of the BSD License. The full license is in
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING.txt, distributed as part of this software.
5 # the file COPYING.txt, distributed as part of this software.
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Verify zmq version dependency >= 2.1.4
9 # Verify zmq version dependency >= 2.1.4
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 import warnings
12 import warnings
13 from distutils.version import LooseVersion as V
13 from distutils.version import LooseVersion as V
14
14
15
16 def patch_pyzmq():
17 """backport a few patches from newer pyzmq
18
19 These can be removed as we bump our minimum pyzmq version
20 """
21
22 import zmq
23
24 # ioloop.install, introduced in pyzmq 2.1.7
25 from zmq.eventloop import ioloop
26
27 def install():
28 import tornado.ioloop
29 tornado.ioloop.IOLoop = ioloop.IOLoop
30
31 if not hasattr(ioloop, 'install'):
32 ioloop.install = install
33
34 # fix missing DEALER/ROUTER aliases in pyzmq < 2.1.9
35 if not hasattr(zmq, 'DEALER'):
36 zmq.DEALER = zmq.XREQ
37 if not hasattr(zmq, 'ROUTER'):
38 zmq.ROUTER = zmq.XREP
39
40 # fallback on stdlib json if jsonlib is selected, because jsonlib breaks things.
41 # jsonlib support is removed from pyzmq >= 2.2.0
42
43 from zmq.utils import jsonapi
44 if jsonapi.jsonmod.__name__ == 'jsonlib':
45 import json
46 jsonapi.jsonmod = json
47
48
15 def check_for_zmq(minimum_version, module='IPython.zmq'):
49 def check_for_zmq(minimum_version, module='IPython.zmq'):
16 try:
50 try:
17 import zmq
51 import zmq
18 except ImportError:
52 except ImportError:
19 raise ImportError("%s requires pyzmq >= %s"%(module, minimum_version))
53 raise ImportError("%s requires pyzmq >= %s"%(module, minimum_version))
20
54
21 pyzmq_version = zmq.__version__
55 pyzmq_version = zmq.__version__
22
56
23 if 'dev' not in pyzmq_version and V(pyzmq_version) < V(minimum_version):
57 if 'dev' not in pyzmq_version and V(pyzmq_version) < V(minimum_version):
24 raise ImportError("%s requires pyzmq >= %s, but you have %s"%(
58 raise ImportError("%s requires pyzmq >= %s, but you have %s"%(
25 module, minimum_version, pyzmq_version))
59 module, minimum_version, pyzmq_version))
26
60
27 # fix missing DEALER/ROUTER aliases in pyzmq < 2.1.9
28 if not hasattr(zmq, 'DEALER'):
29 zmq.DEALER = zmq.XREQ
30 if not hasattr(zmq, 'ROUTER'):
31 zmq.ROUTER = zmq.XREP
32
33 if V(zmq.zmq_version()) >= V('4.0.0'):
61 if V(zmq.zmq_version()) >= V('4.0.0'):
34 warnings.warn("""libzmq 4 detected.
62 warnings.warn("""libzmq 4 detected.
35 It is unlikely that IPython's zmq code will work properly.
63 It is unlikely that IPython's zmq code will work properly.
36 Please install libzmq stable, which is 2.1.x or 2.2.x""",
64 Please install libzmq stable, which is 2.1.x or 2.2.x""",
37 RuntimeWarning)
65 RuntimeWarning)
38
66
39 check_for_zmq('2.1.4')
67 check_for_zmq('2.1.4')
68 patch_pyzmq()
@@ -1,768 +1,756 b''
1 """Session object for building, serializing, sending, and receiving messages in
1 """Session object for building, serializing, sending, and receiving messages in
2 IPython. The Session object supports serialization, HMAC signatures, and
2 IPython. The Session object supports serialization, HMAC signatures, and
3 metadata on messages.
3 metadata on messages.
4
4
5 Also defined here are utilities for working with Sessions:
5 Also defined here are utilities for working with Sessions:
6 * A SessionFactory to be used as a base class for configurables that work with
6 * A SessionFactory to be used as a base class for configurables that work with
7 Sessions.
7 Sessions.
8 * A Message object for convenience that allows attribute-access to the msg dict.
8 * A Message object for convenience that allows attribute-access to the msg dict.
9
9
10 Authors:
10 Authors:
11
11
12 * Min RK
12 * Min RK
13 * Brian Granger
13 * Brian Granger
14 * Fernando Perez
14 * Fernando Perez
15 """
15 """
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Copyright (C) 2010-2011 The IPython Development Team
17 # Copyright (C) 2010-2011 The IPython Development Team
18 #
18 #
19 # Distributed under the terms of the BSD License. The full license is in
19 # Distributed under the terms of the BSD License. The full license is in
20 # the file COPYING, distributed as part of this software.
20 # the file COPYING, distributed as part of this software.
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Imports
24 # Imports
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 import hmac
27 import hmac
28 import logging
28 import logging
29 import os
29 import os
30 import pprint
30 import pprint
31 import uuid
31 import uuid
32 from datetime import datetime
32 from datetime import datetime
33
33
34 try:
34 try:
35 import cPickle
35 import cPickle
36 pickle = cPickle
36 pickle = cPickle
37 except:
37 except:
38 cPickle = None
38 cPickle = None
39 import pickle
39 import pickle
40
40
41 import zmq
41 import zmq
42 from zmq.utils import jsonapi
42 from zmq.utils import jsonapi
43 from zmq.eventloop.ioloop import IOLoop
43 from zmq.eventloop.ioloop import IOLoop
44 from zmq.eventloop.zmqstream import ZMQStream
44 from zmq.eventloop.zmqstream import ZMQStream
45
45
46 from IPython.config.application import Application, boolean_flag
46 from IPython.config.application import Application, boolean_flag
47 from IPython.config.configurable import Configurable, LoggingConfigurable
47 from IPython.config.configurable import Configurable, LoggingConfigurable
48 from IPython.utils.importstring import import_item
48 from IPython.utils.importstring import import_item
49 from IPython.utils.jsonutil import extract_dates, squash_dates, date_default
49 from IPython.utils.jsonutil import extract_dates, squash_dates, date_default
50 from IPython.utils.py3compat import str_to_bytes
50 from IPython.utils.py3compat import str_to_bytes
51 from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set,
51 from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set,
52 DottedObjectName, CUnicode)
52 DottedObjectName, CUnicode)
53
53
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55 # utility functions
55 # utility functions
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57
57
58 def squash_unicode(obj):
58 def squash_unicode(obj):
59 """coerce unicode back to bytestrings."""
59 """coerce unicode back to bytestrings."""
60 if isinstance(obj,dict):
60 if isinstance(obj,dict):
61 for key in obj.keys():
61 for key in obj.keys():
62 obj[key] = squash_unicode(obj[key])
62 obj[key] = squash_unicode(obj[key])
63 if isinstance(key, unicode):
63 if isinstance(key, unicode):
64 obj[squash_unicode(key)] = obj.pop(key)
64 obj[squash_unicode(key)] = obj.pop(key)
65 elif isinstance(obj, list):
65 elif isinstance(obj, list):
66 for i,v in enumerate(obj):
66 for i,v in enumerate(obj):
67 obj[i] = squash_unicode(v)
67 obj[i] = squash_unicode(v)
68 elif isinstance(obj, unicode):
68 elif isinstance(obj, unicode):
69 obj = obj.encode('utf8')
69 obj = obj.encode('utf8')
70 return obj
70 return obj
71
71
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73 # globals and defaults
73 # globals and defaults
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75
75
76
76
77 # jsonlib behaves a bit differently, so handle that where it affects us
77 # ISO8601-ify datetime objects
78 if jsonapi.jsonmod.__name__ == 'jsonlib':
78 json_packer = lambda obj: jsonapi.dumps(obj, default=date_default)
79 # kwarg for serializing unknown types (datetime) is different
79 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
80 dumps_kwargs = dict(on_unknown=date_default)
81 # By default, jsonlib unpacks floats as Decimal instead of float,
82 # which can foul things up
83 loads_kwargs = dict(use_float=True)
84 else:
85 # ISO8601-ify datetime objects
86 dumps_kwargs = dict(default=date_default)
87 # nothing to specify for loads
88 loads_kwargs = dict()
89
90 json_packer = lambda obj: jsonapi.dumps(obj, **dumps_kwargs)
91 json_unpacker = lambda s: extract_dates(jsonapi.loads(s, **loads_kwargs))
92
80
93 pickle_packer = lambda o: pickle.dumps(o,-1)
81 pickle_packer = lambda o: pickle.dumps(o,-1)
94 pickle_unpacker = pickle.loads
82 pickle_unpacker = pickle.loads
95
83
96 default_packer = json_packer
84 default_packer = json_packer
97 default_unpacker = json_unpacker
85 default_unpacker = json_unpacker
98
86
99 DELIM=b"<IDS|MSG>"
87 DELIM=b"<IDS|MSG>"
100
88
101
89
102 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
103 # Mixin tools for apps that use Sessions
91 # Mixin tools for apps that use Sessions
104 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
105
93
106 session_aliases = dict(
94 session_aliases = dict(
107 ident = 'Session.session',
95 ident = 'Session.session',
108 user = 'Session.username',
96 user = 'Session.username',
109 keyfile = 'Session.keyfile',
97 keyfile = 'Session.keyfile',
110 )
98 )
111
99
112 session_flags = {
100 session_flags = {
113 'secure' : ({'Session' : { 'key' : str_to_bytes(str(uuid.uuid4())),
101 'secure' : ({'Session' : { 'key' : str_to_bytes(str(uuid.uuid4())),
114 'keyfile' : '' }},
102 'keyfile' : '' }},
115 """Use HMAC digests for authentication of messages.
103 """Use HMAC digests for authentication of messages.
116 Setting this flag will generate a new UUID to use as the HMAC key.
104 Setting this flag will generate a new UUID to use as the HMAC key.
117 """),
105 """),
118 'no-secure' : ({'Session' : { 'key' : b'', 'keyfile' : '' }},
106 'no-secure' : ({'Session' : { 'key' : b'', 'keyfile' : '' }},
119 """Don't authenticate messages."""),
107 """Don't authenticate messages."""),
120 }
108 }
121
109
122 def default_secure(cfg):
110 def default_secure(cfg):
123 """Set the default behavior for a config environment to be secure.
111 """Set the default behavior for a config environment to be secure.
124
112
125 If Session.key/keyfile have not been set, set Session.key to
113 If Session.key/keyfile have not been set, set Session.key to
126 a new random UUID.
114 a new random UUID.
127 """
115 """
128
116
129 if 'Session' in cfg:
117 if 'Session' in cfg:
130 if 'key' in cfg.Session or 'keyfile' in cfg.Session:
118 if 'key' in cfg.Session or 'keyfile' in cfg.Session:
131 return
119 return
132 # key/keyfile not specified, generate new UUID:
120 # key/keyfile not specified, generate new UUID:
133 cfg.Session.key = str_to_bytes(str(uuid.uuid4()))
121 cfg.Session.key = str_to_bytes(str(uuid.uuid4()))
134
122
135
123
136 #-----------------------------------------------------------------------------
124 #-----------------------------------------------------------------------------
137 # Classes
125 # Classes
138 #-----------------------------------------------------------------------------
126 #-----------------------------------------------------------------------------
139
127
140 class SessionFactory(LoggingConfigurable):
128 class SessionFactory(LoggingConfigurable):
141 """The Base class for configurables that have a Session, Context, logger,
129 """The Base class for configurables that have a Session, Context, logger,
142 and IOLoop.
130 and IOLoop.
143 """
131 """
144
132
145 logname = Unicode('')
133 logname = Unicode('')
146 def _logname_changed(self, name, old, new):
134 def _logname_changed(self, name, old, new):
147 self.log = logging.getLogger(new)
135 self.log = logging.getLogger(new)
148
136
149 # not configurable:
137 # not configurable:
150 context = Instance('zmq.Context')
138 context = Instance('zmq.Context')
151 def _context_default(self):
139 def _context_default(self):
152 return zmq.Context.instance()
140 return zmq.Context.instance()
153
141
154 session = Instance('IPython.zmq.session.Session')
142 session = Instance('IPython.zmq.session.Session')
155
143
156 loop = Instance('zmq.eventloop.ioloop.IOLoop', allow_none=False)
144 loop = Instance('zmq.eventloop.ioloop.IOLoop', allow_none=False)
157 def _loop_default(self):
145 def _loop_default(self):
158 return IOLoop.instance()
146 return IOLoop.instance()
159
147
160 def __init__(self, **kwargs):
148 def __init__(self, **kwargs):
161 super(SessionFactory, self).__init__(**kwargs)
149 super(SessionFactory, self).__init__(**kwargs)
162
150
163 if self.session is None:
151 if self.session is None:
164 # construct the session
152 # construct the session
165 self.session = Session(**kwargs)
153 self.session = Session(**kwargs)
166
154
167
155
168 class Message(object):
156 class Message(object):
169 """A simple message object that maps dict keys to attributes.
157 """A simple message object that maps dict keys to attributes.
170
158
171 A Message can be created from a dict and a dict from a Message instance
159 A Message can be created from a dict and a dict from a Message instance
172 simply by calling dict(msg_obj)."""
160 simply by calling dict(msg_obj)."""
173
161
174 def __init__(self, msg_dict):
162 def __init__(self, msg_dict):
175 dct = self.__dict__
163 dct = self.__dict__
176 for k, v in dict(msg_dict).iteritems():
164 for k, v in dict(msg_dict).iteritems():
177 if isinstance(v, dict):
165 if isinstance(v, dict):
178 v = Message(v)
166 v = Message(v)
179 dct[k] = v
167 dct[k] = v
180
168
181 # Having this iterator lets dict(msg_obj) work out of the box.
169 # Having this iterator lets dict(msg_obj) work out of the box.
182 def __iter__(self):
170 def __iter__(self):
183 return iter(self.__dict__.iteritems())
171 return iter(self.__dict__.iteritems())
184
172
185 def __repr__(self):
173 def __repr__(self):
186 return repr(self.__dict__)
174 return repr(self.__dict__)
187
175
188 def __str__(self):
176 def __str__(self):
189 return pprint.pformat(self.__dict__)
177 return pprint.pformat(self.__dict__)
190
178
191 def __contains__(self, k):
179 def __contains__(self, k):
192 return k in self.__dict__
180 return k in self.__dict__
193
181
194 def __getitem__(self, k):
182 def __getitem__(self, k):
195 return self.__dict__[k]
183 return self.__dict__[k]
196
184
197
185
198 def msg_header(msg_id, msg_type, username, session):
186 def msg_header(msg_id, msg_type, username, session):
199 date = datetime.now()
187 date = datetime.now()
200 return locals()
188 return locals()
201
189
202 def extract_header(msg_or_header):
190 def extract_header(msg_or_header):
203 """Given a message or header, return the header."""
191 """Given a message or header, return the header."""
204 if not msg_or_header:
192 if not msg_or_header:
205 return {}
193 return {}
206 try:
194 try:
207 # See if msg_or_header is the entire message.
195 # See if msg_or_header is the entire message.
208 h = msg_or_header['header']
196 h = msg_or_header['header']
209 except KeyError:
197 except KeyError:
210 try:
198 try:
211 # See if msg_or_header is just the header
199 # See if msg_or_header is just the header
212 h = msg_or_header['msg_id']
200 h = msg_or_header['msg_id']
213 except KeyError:
201 except KeyError:
214 raise
202 raise
215 else:
203 else:
216 h = msg_or_header
204 h = msg_or_header
217 if not isinstance(h, dict):
205 if not isinstance(h, dict):
218 h = dict(h)
206 h = dict(h)
219 return h
207 return h
220
208
221 class Session(Configurable):
209 class Session(Configurable):
222 """Object for handling serialization and sending of messages.
210 """Object for handling serialization and sending of messages.
223
211
224 The Session object handles building messages and sending them
212 The Session object handles building messages and sending them
225 with ZMQ sockets or ZMQStream objects. Objects can communicate with each
213 with ZMQ sockets or ZMQStream objects. Objects can communicate with each
226 other over the network via Session objects, and only need to work with the
214 other over the network via Session objects, and only need to work with the
227 dict-based IPython message spec. The Session will handle
215 dict-based IPython message spec. The Session will handle
228 serialization/deserialization, security, and metadata.
216 serialization/deserialization, security, and metadata.
229
217
230 Sessions support configurable serialiization via packer/unpacker traits,
218 Sessions support configurable serialiization via packer/unpacker traits,
231 and signing with HMAC digests via the key/keyfile traits.
219 and signing with HMAC digests via the key/keyfile traits.
232
220
233 Parameters
221 Parameters
234 ----------
222 ----------
235
223
236 debug : bool
224 debug : bool
237 whether to trigger extra debugging statements
225 whether to trigger extra debugging statements
238 packer/unpacker : str : 'json', 'pickle' or import_string
226 packer/unpacker : str : 'json', 'pickle' or import_string
239 importstrings for methods to serialize message parts. If just
227 importstrings for methods to serialize message parts. If just
240 'json' or 'pickle', predefined JSON and pickle packers will be used.
228 'json' or 'pickle', predefined JSON and pickle packers will be used.
241 Otherwise, the entire importstring must be used.
229 Otherwise, the entire importstring must be used.
242
230
243 The functions must accept at least valid JSON input, and output *bytes*.
231 The functions must accept at least valid JSON input, and output *bytes*.
244
232
245 For example, to use msgpack:
233 For example, to use msgpack:
246 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
234 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
247 pack/unpack : callables
235 pack/unpack : callables
248 You can also set the pack/unpack callables for serialization directly.
236 You can also set the pack/unpack callables for serialization directly.
249 session : bytes
237 session : bytes
250 the ID of this Session object. The default is to generate a new UUID.
238 the ID of this Session object. The default is to generate a new UUID.
251 username : unicode
239 username : unicode
252 username added to message headers. The default is to ask the OS.
240 username added to message headers. The default is to ask the OS.
253 key : bytes
241 key : bytes
254 The key used to initialize an HMAC signature. If unset, messages
242 The key used to initialize an HMAC signature. If unset, messages
255 will not be signed or checked.
243 will not be signed or checked.
256 keyfile : filepath
244 keyfile : filepath
257 The file containing a key. If this is set, `key` will be initialized
245 The file containing a key. If this is set, `key` will be initialized
258 to the contents of the file.
246 to the contents of the file.
259
247
260 """
248 """
261
249
262 debug=Bool(False, config=True, help="""Debug output in the Session""")
250 debug=Bool(False, config=True, help="""Debug output in the Session""")
263
251
264 packer = DottedObjectName('json',config=True,
252 packer = DottedObjectName('json',config=True,
265 help="""The name of the packer for serializing messages.
253 help="""The name of the packer for serializing messages.
266 Should be one of 'json', 'pickle', or an import name
254 Should be one of 'json', 'pickle', or an import name
267 for a custom callable serializer.""")
255 for a custom callable serializer.""")
268 def _packer_changed(self, name, old, new):
256 def _packer_changed(self, name, old, new):
269 if new.lower() == 'json':
257 if new.lower() == 'json':
270 self.pack = json_packer
258 self.pack = json_packer
271 self.unpack = json_unpacker
259 self.unpack = json_unpacker
272 elif new.lower() == 'pickle':
260 elif new.lower() == 'pickle':
273 self.pack = pickle_packer
261 self.pack = pickle_packer
274 self.unpack = pickle_unpacker
262 self.unpack = pickle_unpacker
275 else:
263 else:
276 self.pack = import_item(str(new))
264 self.pack = import_item(str(new))
277
265
278 unpacker = DottedObjectName('json', config=True,
266 unpacker = DottedObjectName('json', config=True,
279 help="""The name of the unpacker for unserializing messages.
267 help="""The name of the unpacker for unserializing messages.
280 Only used with custom functions for `packer`.""")
268 Only used with custom functions for `packer`.""")
281 def _unpacker_changed(self, name, old, new):
269 def _unpacker_changed(self, name, old, new):
282 if new.lower() == 'json':
270 if new.lower() == 'json':
283 self.pack = json_packer
271 self.pack = json_packer
284 self.unpack = json_unpacker
272 self.unpack = json_unpacker
285 elif new.lower() == 'pickle':
273 elif new.lower() == 'pickle':
286 self.pack = pickle_packer
274 self.pack = pickle_packer
287 self.unpack = pickle_unpacker
275 self.unpack = pickle_unpacker
288 else:
276 else:
289 self.unpack = import_item(str(new))
277 self.unpack = import_item(str(new))
290
278
291 session = CUnicode(u'', config=True,
279 session = CUnicode(u'', config=True,
292 help="""The UUID identifying this session.""")
280 help="""The UUID identifying this session.""")
293 def _session_default(self):
281 def _session_default(self):
294 u = unicode(uuid.uuid4())
282 u = unicode(uuid.uuid4())
295 self.bsession = u.encode('ascii')
283 self.bsession = u.encode('ascii')
296 return u
284 return u
297
285
298 def _session_changed(self, name, old, new):
286 def _session_changed(self, name, old, new):
299 self.bsession = self.session.encode('ascii')
287 self.bsession = self.session.encode('ascii')
300
288
301 # bsession is the session as bytes
289 # bsession is the session as bytes
302 bsession = CBytes(b'')
290 bsession = CBytes(b'')
303
291
304 username = Unicode(os.environ.get('USER',u'username'), config=True,
292 username = Unicode(os.environ.get('USER',u'username'), config=True,
305 help="""Username for the Session. Default is your system username.""")
293 help="""Username for the Session. Default is your system username.""")
306
294
307 # message signature related traits:
295 # message signature related traits:
308
296
309 key = CBytes(b'', config=True,
297 key = CBytes(b'', config=True,
310 help="""execution key, for extra authentication.""")
298 help="""execution key, for extra authentication.""")
311 def _key_changed(self, name, old, new):
299 def _key_changed(self, name, old, new):
312 if new:
300 if new:
313 self.auth = hmac.HMAC(new)
301 self.auth = hmac.HMAC(new)
314 else:
302 else:
315 self.auth = None
303 self.auth = None
316 auth = Instance(hmac.HMAC)
304 auth = Instance(hmac.HMAC)
317 digest_history = Set()
305 digest_history = Set()
318
306
319 keyfile = Unicode('', config=True,
307 keyfile = Unicode('', config=True,
320 help="""path to file containing execution key.""")
308 help="""path to file containing execution key.""")
321 def _keyfile_changed(self, name, old, new):
309 def _keyfile_changed(self, name, old, new):
322 with open(new, 'rb') as f:
310 with open(new, 'rb') as f:
323 self.key = f.read().strip()
311 self.key = f.read().strip()
324
312
325 # serialization traits:
313 # serialization traits:
326
314
327 pack = Any(default_packer) # the actual packer function
315 pack = Any(default_packer) # the actual packer function
328 def _pack_changed(self, name, old, new):
316 def _pack_changed(self, name, old, new):
329 if not callable(new):
317 if not callable(new):
330 raise TypeError("packer must be callable, not %s"%type(new))
318 raise TypeError("packer must be callable, not %s"%type(new))
331
319
332 unpack = Any(default_unpacker) # the actual packer function
320 unpack = Any(default_unpacker) # the actual packer function
333 def _unpack_changed(self, name, old, new):
321 def _unpack_changed(self, name, old, new):
334 # unpacker is not checked - it is assumed to be
322 # unpacker is not checked - it is assumed to be
335 if not callable(new):
323 if not callable(new):
336 raise TypeError("unpacker must be callable, not %s"%type(new))
324 raise TypeError("unpacker must be callable, not %s"%type(new))
337
325
338 def __init__(self, **kwargs):
326 def __init__(self, **kwargs):
339 """create a Session object
327 """create a Session object
340
328
341 Parameters
329 Parameters
342 ----------
330 ----------
343
331
344 debug : bool
332 debug : bool
345 whether to trigger extra debugging statements
333 whether to trigger extra debugging statements
346 packer/unpacker : str : 'json', 'pickle' or import_string
334 packer/unpacker : str : 'json', 'pickle' or import_string
347 importstrings for methods to serialize message parts. If just
335 importstrings for methods to serialize message parts. If just
348 'json' or 'pickle', predefined JSON and pickle packers will be used.
336 'json' or 'pickle', predefined JSON and pickle packers will be used.
349 Otherwise, the entire importstring must be used.
337 Otherwise, the entire importstring must be used.
350
338
351 The functions must accept at least valid JSON input, and output
339 The functions must accept at least valid JSON input, and output
352 *bytes*.
340 *bytes*.
353
341
354 For example, to use msgpack:
342 For example, to use msgpack:
355 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
343 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
356 pack/unpack : callables
344 pack/unpack : callables
357 You can also set the pack/unpack callables for serialization
345 You can also set the pack/unpack callables for serialization
358 directly.
346 directly.
359 session : unicode (must be ascii)
347 session : unicode (must be ascii)
360 the ID of this Session object. The default is to generate a new
348 the ID of this Session object. The default is to generate a new
361 UUID.
349 UUID.
362 bsession : bytes
350 bsession : bytes
363 The session as bytes
351 The session as bytes
364 username : unicode
352 username : unicode
365 username added to message headers. The default is to ask the OS.
353 username added to message headers. The default is to ask the OS.
366 key : bytes
354 key : bytes
367 The key used to initialize an HMAC signature. If unset, messages
355 The key used to initialize an HMAC signature. If unset, messages
368 will not be signed or checked.
356 will not be signed or checked.
369 keyfile : filepath
357 keyfile : filepath
370 The file containing a key. If this is set, `key` will be
358 The file containing a key. If this is set, `key` will be
371 initialized to the contents of the file.
359 initialized to the contents of the file.
372 """
360 """
373 super(Session, self).__init__(**kwargs)
361 super(Session, self).__init__(**kwargs)
374 self._check_packers()
362 self._check_packers()
375 self.none = self.pack({})
363 self.none = self.pack({})
376 # ensure self._session_default() if necessary, so bsession is defined:
364 # ensure self._session_default() if necessary, so bsession is defined:
377 self.session
365 self.session
378
366
379 @property
367 @property
380 def msg_id(self):
368 def msg_id(self):
381 """always return new uuid"""
369 """always return new uuid"""
382 return str(uuid.uuid4())
370 return str(uuid.uuid4())
383
371
384 def _check_packers(self):
372 def _check_packers(self):
385 """check packers for binary data and datetime support."""
373 """check packers for binary data and datetime support."""
386 pack = self.pack
374 pack = self.pack
387 unpack = self.unpack
375 unpack = self.unpack
388
376
389 # check simple serialization
377 # check simple serialization
390 msg = dict(a=[1,'hi'])
378 msg = dict(a=[1,'hi'])
391 try:
379 try:
392 packed = pack(msg)
380 packed = pack(msg)
393 except Exception:
381 except Exception:
394 raise ValueError("packer could not serialize a simple message")
382 raise ValueError("packer could not serialize a simple message")
395
383
396 # ensure packed message is bytes
384 # ensure packed message is bytes
397 if not isinstance(packed, bytes):
385 if not isinstance(packed, bytes):
398 raise ValueError("message packed to %r, but bytes are required"%type(packed))
386 raise ValueError("message packed to %r, but bytes are required"%type(packed))
399
387
400 # check that unpack is pack's inverse
388 # check that unpack is pack's inverse
401 try:
389 try:
402 unpacked = unpack(packed)
390 unpacked = unpack(packed)
403 except Exception:
391 except Exception:
404 raise ValueError("unpacker could not handle the packer's output")
392 raise ValueError("unpacker could not handle the packer's output")
405
393
406 # check datetime support
394 # check datetime support
407 msg = dict(t=datetime.now())
395 msg = dict(t=datetime.now())
408 try:
396 try:
409 unpacked = unpack(pack(msg))
397 unpacked = unpack(pack(msg))
410 except Exception:
398 except Exception:
411 self.pack = lambda o: pack(squash_dates(o))
399 self.pack = lambda o: pack(squash_dates(o))
412 self.unpack = lambda s: extract_dates(unpack(s))
400 self.unpack = lambda s: extract_dates(unpack(s))
413
401
414 def msg_header(self, msg_type):
402 def msg_header(self, msg_type):
415 return msg_header(self.msg_id, msg_type, self.username, self.session)
403 return msg_header(self.msg_id, msg_type, self.username, self.session)
416
404
417 def msg(self, msg_type, content=None, parent=None, subheader=None, header=None):
405 def msg(self, msg_type, content=None, parent=None, subheader=None, header=None):
418 """Return the nested message dict.
406 """Return the nested message dict.
419
407
420 This format is different from what is sent over the wire. The
408 This format is different from what is sent over the wire. The
421 serialize/unserialize methods converts this nested message dict to the wire
409 serialize/unserialize methods converts this nested message dict to the wire
422 format, which is a list of message parts.
410 format, which is a list of message parts.
423 """
411 """
424 msg = {}
412 msg = {}
425 header = self.msg_header(msg_type) if header is None else header
413 header = self.msg_header(msg_type) if header is None else header
426 msg['header'] = header
414 msg['header'] = header
427 msg['msg_id'] = header['msg_id']
415 msg['msg_id'] = header['msg_id']
428 msg['msg_type'] = header['msg_type']
416 msg['msg_type'] = header['msg_type']
429 msg['parent_header'] = {} if parent is None else extract_header(parent)
417 msg['parent_header'] = {} if parent is None else extract_header(parent)
430 msg['content'] = {} if content is None else content
418 msg['content'] = {} if content is None else content
431 sub = {} if subheader is None else subheader
419 sub = {} if subheader is None else subheader
432 msg['header'].update(sub)
420 msg['header'].update(sub)
433 return msg
421 return msg
434
422
435 def sign(self, msg_list):
423 def sign(self, msg_list):
436 """Sign a message with HMAC digest. If no auth, return b''.
424 """Sign a message with HMAC digest. If no auth, return b''.
437
425
438 Parameters
426 Parameters
439 ----------
427 ----------
440 msg_list : list
428 msg_list : list
441 The [p_header,p_parent,p_content] part of the message list.
429 The [p_header,p_parent,p_content] part of the message list.
442 """
430 """
443 if self.auth is None:
431 if self.auth is None:
444 return b''
432 return b''
445 h = self.auth.copy()
433 h = self.auth.copy()
446 for m in msg_list:
434 for m in msg_list:
447 h.update(m)
435 h.update(m)
448 return str_to_bytes(h.hexdigest())
436 return str_to_bytes(h.hexdigest())
449
437
450 def serialize(self, msg, ident=None):
438 def serialize(self, msg, ident=None):
451 """Serialize the message components to bytes.
439 """Serialize the message components to bytes.
452
440
453 This is roughly the inverse of unserialize. The serialize/unserialize
441 This is roughly the inverse of unserialize. The serialize/unserialize
454 methods work with full message lists, whereas pack/unpack work with
442 methods work with full message lists, whereas pack/unpack work with
455 the individual message parts in the message list.
443 the individual message parts in the message list.
456
444
457 Parameters
445 Parameters
458 ----------
446 ----------
459 msg : dict or Message
447 msg : dict or Message
460 The nexted message dict as returned by the self.msg method.
448 The nexted message dict as returned by the self.msg method.
461
449
462 Returns
450 Returns
463 -------
451 -------
464 msg_list : list
452 msg_list : list
465 The list of bytes objects to be sent with the format:
453 The list of bytes objects to be sent with the format:
466 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
454 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
467 buffer1,buffer2,...]. In this list, the p_* entities are
455 buffer1,buffer2,...]. In this list, the p_* entities are
468 the packed or serialized versions, so if JSON is used, these
456 the packed or serialized versions, so if JSON is used, these
469 are utf8 encoded JSON strings.
457 are utf8 encoded JSON strings.
470 """
458 """
471 content = msg.get('content', {})
459 content = msg.get('content', {})
472 if content is None:
460 if content is None:
473 content = self.none
461 content = self.none
474 elif isinstance(content, dict):
462 elif isinstance(content, dict):
475 content = self.pack(content)
463 content = self.pack(content)
476 elif isinstance(content, bytes):
464 elif isinstance(content, bytes):
477 # content is already packed, as in a relayed message
465 # content is already packed, as in a relayed message
478 pass
466 pass
479 elif isinstance(content, unicode):
467 elif isinstance(content, unicode):
480 # should be bytes, but JSON often spits out unicode
468 # should be bytes, but JSON often spits out unicode
481 content = content.encode('utf8')
469 content = content.encode('utf8')
482 else:
470 else:
483 raise TypeError("Content incorrect type: %s"%type(content))
471 raise TypeError("Content incorrect type: %s"%type(content))
484
472
485 real_message = [self.pack(msg['header']),
473 real_message = [self.pack(msg['header']),
486 self.pack(msg['parent_header']),
474 self.pack(msg['parent_header']),
487 content
475 content
488 ]
476 ]
489
477
490 to_send = []
478 to_send = []
491
479
492 if isinstance(ident, list):
480 if isinstance(ident, list):
493 # accept list of idents
481 # accept list of idents
494 to_send.extend(ident)
482 to_send.extend(ident)
495 elif ident is not None:
483 elif ident is not None:
496 to_send.append(ident)
484 to_send.append(ident)
497 to_send.append(DELIM)
485 to_send.append(DELIM)
498
486
499 signature = self.sign(real_message)
487 signature = self.sign(real_message)
500 to_send.append(signature)
488 to_send.append(signature)
501
489
502 to_send.extend(real_message)
490 to_send.extend(real_message)
503
491
504 return to_send
492 return to_send
505
493
506 def send(self, stream, msg_or_type, content=None, parent=None, ident=None,
494 def send(self, stream, msg_or_type, content=None, parent=None, ident=None,
507 buffers=None, subheader=None, track=False, header=None):
495 buffers=None, subheader=None, track=False, header=None):
508 """Build and send a message via stream or socket.
496 """Build and send a message via stream or socket.
509
497
510 The message format used by this function internally is as follows:
498 The message format used by this function internally is as follows:
511
499
512 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
500 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
513 buffer1,buffer2,...]
501 buffer1,buffer2,...]
514
502
515 The serialize/unserialize methods convert the nested message dict into this
503 The serialize/unserialize methods convert the nested message dict into this
516 format.
504 format.
517
505
518 Parameters
506 Parameters
519 ----------
507 ----------
520
508
521 stream : zmq.Socket or ZMQStream
509 stream : zmq.Socket or ZMQStream
522 The socket-like object used to send the data.
510 The socket-like object used to send the data.
523 msg_or_type : str or Message/dict
511 msg_or_type : str or Message/dict
524 Normally, msg_or_type will be a msg_type unless a message is being
512 Normally, msg_or_type will be a msg_type unless a message is being
525 sent more than once. If a header is supplied, this can be set to
513 sent more than once. If a header is supplied, this can be set to
526 None and the msg_type will be pulled from the header.
514 None and the msg_type will be pulled from the header.
527
515
528 content : dict or None
516 content : dict or None
529 The content of the message (ignored if msg_or_type is a message).
517 The content of the message (ignored if msg_or_type is a message).
530 header : dict or None
518 header : dict or None
531 The header dict for the message (ignores if msg_to_type is a message).
519 The header dict for the message (ignores if msg_to_type is a message).
532 parent : Message or dict or None
520 parent : Message or dict or None
533 The parent or parent header describing the parent of this message
521 The parent or parent header describing the parent of this message
534 (ignored if msg_or_type is a message).
522 (ignored if msg_or_type is a message).
535 ident : bytes or list of bytes
523 ident : bytes or list of bytes
536 The zmq.IDENTITY routing path.
524 The zmq.IDENTITY routing path.
537 subheader : dict or None
525 subheader : dict or None
538 Extra header keys for this message's header (ignored if msg_or_type
526 Extra header keys for this message's header (ignored if msg_or_type
539 is a message).
527 is a message).
540 buffers : list or None
528 buffers : list or None
541 The already-serialized buffers to be appended to the message.
529 The already-serialized buffers to be appended to the message.
542 track : bool
530 track : bool
543 Whether to track. Only for use with Sockets, because ZMQStream
531 Whether to track. Only for use with Sockets, because ZMQStream
544 objects cannot track messages.
532 objects cannot track messages.
545
533
546 Returns
534 Returns
547 -------
535 -------
548 msg : dict
536 msg : dict
549 The constructed message.
537 The constructed message.
550 (msg,tracker) : (dict, MessageTracker)
538 (msg,tracker) : (dict, MessageTracker)
551 if track=True, then a 2-tuple will be returned,
539 if track=True, then a 2-tuple will be returned,
552 the first element being the constructed
540 the first element being the constructed
553 message, and the second being the MessageTracker
541 message, and the second being the MessageTracker
554
542
555 """
543 """
556
544
557 if not isinstance(stream, (zmq.Socket, ZMQStream)):
545 if not isinstance(stream, (zmq.Socket, ZMQStream)):
558 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
546 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
559 elif track and isinstance(stream, ZMQStream):
547 elif track and isinstance(stream, ZMQStream):
560 raise TypeError("ZMQStream cannot track messages")
548 raise TypeError("ZMQStream cannot track messages")
561
549
562 if isinstance(msg_or_type, (Message, dict)):
550 if isinstance(msg_or_type, (Message, dict)):
563 # We got a Message or message dict, not a msg_type so don't
551 # We got a Message or message dict, not a msg_type so don't
564 # build a new Message.
552 # build a new Message.
565 msg = msg_or_type
553 msg = msg_or_type
566 else:
554 else:
567 msg = self.msg(msg_or_type, content=content, parent=parent,
555 msg = self.msg(msg_or_type, content=content, parent=parent,
568 subheader=subheader, header=header)
556 subheader=subheader, header=header)
569
557
570 buffers = [] if buffers is None else buffers
558 buffers = [] if buffers is None else buffers
571 to_send = self.serialize(msg, ident)
559 to_send = self.serialize(msg, ident)
572 flag = 0
560 flag = 0
573 if buffers:
561 if buffers:
574 flag = zmq.SNDMORE
562 flag = zmq.SNDMORE
575 _track = False
563 _track = False
576 else:
564 else:
577 _track=track
565 _track=track
578 if track:
566 if track:
579 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
567 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
580 else:
568 else:
581 tracker = stream.send_multipart(to_send, flag, copy=False)
569 tracker = stream.send_multipart(to_send, flag, copy=False)
582 for b in buffers[:-1]:
570 for b in buffers[:-1]:
583 stream.send(b, flag, copy=False)
571 stream.send(b, flag, copy=False)
584 if buffers:
572 if buffers:
585 if track:
573 if track:
586 tracker = stream.send(buffers[-1], copy=False, track=track)
574 tracker = stream.send(buffers[-1], copy=False, track=track)
587 else:
575 else:
588 tracker = stream.send(buffers[-1], copy=False)
576 tracker = stream.send(buffers[-1], copy=False)
589
577
590 # omsg = Message(msg)
578 # omsg = Message(msg)
591 if self.debug:
579 if self.debug:
592 pprint.pprint(msg)
580 pprint.pprint(msg)
593 pprint.pprint(to_send)
581 pprint.pprint(to_send)
594 pprint.pprint(buffers)
582 pprint.pprint(buffers)
595
583
596 msg['tracker'] = tracker
584 msg['tracker'] = tracker
597
585
598 return msg
586 return msg
599
587
600 def send_raw(self, stream, msg_list, flags=0, copy=True, ident=None):
588 def send_raw(self, stream, msg_list, flags=0, copy=True, ident=None):
601 """Send a raw message via ident path.
589 """Send a raw message via ident path.
602
590
603 This method is used to send a already serialized message.
591 This method is used to send a already serialized message.
604
592
605 Parameters
593 Parameters
606 ----------
594 ----------
607 stream : ZMQStream or Socket
595 stream : ZMQStream or Socket
608 The ZMQ stream or socket to use for sending the message.
596 The ZMQ stream or socket to use for sending the message.
609 msg_list : list
597 msg_list : list
610 The serialized list of messages to send. This only includes the
598 The serialized list of messages to send. This only includes the
611 [p_header,p_parent,p_content,buffer1,buffer2,...] portion of
599 [p_header,p_parent,p_content,buffer1,buffer2,...] portion of
612 the message.
600 the message.
613 ident : ident or list
601 ident : ident or list
614 A single ident or a list of idents to use in sending.
602 A single ident or a list of idents to use in sending.
615 """
603 """
616 to_send = []
604 to_send = []
617 if isinstance(ident, bytes):
605 if isinstance(ident, bytes):
618 ident = [ident]
606 ident = [ident]
619 if ident is not None:
607 if ident is not None:
620 to_send.extend(ident)
608 to_send.extend(ident)
621
609
622 to_send.append(DELIM)
610 to_send.append(DELIM)
623 to_send.append(self.sign(msg_list))
611 to_send.append(self.sign(msg_list))
624 to_send.extend(msg_list)
612 to_send.extend(msg_list)
625 stream.send_multipart(msg_list, flags, copy=copy)
613 stream.send_multipart(msg_list, flags, copy=copy)
626
614
627 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
615 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
628 """Receive and unpack a message.
616 """Receive and unpack a message.
629
617
630 Parameters
618 Parameters
631 ----------
619 ----------
632 socket : ZMQStream or Socket
620 socket : ZMQStream or Socket
633 The socket or stream to use in receiving.
621 The socket or stream to use in receiving.
634
622
635 Returns
623 Returns
636 -------
624 -------
637 [idents], msg
625 [idents], msg
638 [idents] is a list of idents and msg is a nested message dict of
626 [idents] is a list of idents and msg is a nested message dict of
639 same format as self.msg returns.
627 same format as self.msg returns.
640 """
628 """
641 if isinstance(socket, ZMQStream):
629 if isinstance(socket, ZMQStream):
642 socket = socket.socket
630 socket = socket.socket
643 try:
631 try:
644 msg_list = socket.recv_multipart(mode)
632 msg_list = socket.recv_multipart(mode)
645 except zmq.ZMQError as e:
633 except zmq.ZMQError as e:
646 if e.errno == zmq.EAGAIN:
634 if e.errno == zmq.EAGAIN:
647 # We can convert EAGAIN to None as we know in this case
635 # We can convert EAGAIN to None as we know in this case
648 # recv_multipart won't return None.
636 # recv_multipart won't return None.
649 return None,None
637 return None,None
650 else:
638 else:
651 raise
639 raise
652 # split multipart message into identity list and message dict
640 # split multipart message into identity list and message dict
653 # invalid large messages can cause very expensive string comparisons
641 # invalid large messages can cause very expensive string comparisons
654 idents, msg_list = self.feed_identities(msg_list, copy)
642 idents, msg_list = self.feed_identities(msg_list, copy)
655 try:
643 try:
656 return idents, self.unserialize(msg_list, content=content, copy=copy)
644 return idents, self.unserialize(msg_list, content=content, copy=copy)
657 except Exception as e:
645 except Exception as e:
658 # TODO: handle it
646 # TODO: handle it
659 raise e
647 raise e
660
648
661 def feed_identities(self, msg_list, copy=True):
649 def feed_identities(self, msg_list, copy=True):
662 """Split the identities from the rest of the message.
650 """Split the identities from the rest of the message.
663
651
664 Feed until DELIM is reached, then return the prefix as idents and
652 Feed until DELIM is reached, then return the prefix as idents and
665 remainder as msg_list. This is easily broken by setting an IDENT to DELIM,
653 remainder as msg_list. This is easily broken by setting an IDENT to DELIM,
666 but that would be silly.
654 but that would be silly.
667
655
668 Parameters
656 Parameters
669 ----------
657 ----------
670 msg_list : a list of Message or bytes objects
658 msg_list : a list of Message or bytes objects
671 The message to be split.
659 The message to be split.
672 copy : bool
660 copy : bool
673 flag determining whether the arguments are bytes or Messages
661 flag determining whether the arguments are bytes or Messages
674
662
675 Returns
663 Returns
676 -------
664 -------
677 (idents, msg_list) : two lists
665 (idents, msg_list) : two lists
678 idents will always be a list of bytes, each of which is a ZMQ
666 idents will always be a list of bytes, each of which is a ZMQ
679 identity. msg_list will be a list of bytes or zmq.Messages of the
667 identity. msg_list will be a list of bytes or zmq.Messages of the
680 form [HMAC,p_header,p_parent,p_content,buffer1,buffer2,...] and
668 form [HMAC,p_header,p_parent,p_content,buffer1,buffer2,...] and
681 should be unpackable/unserializable via self.unserialize at this
669 should be unpackable/unserializable via self.unserialize at this
682 point.
670 point.
683 """
671 """
684 if copy:
672 if copy:
685 idx = msg_list.index(DELIM)
673 idx = msg_list.index(DELIM)
686 return msg_list[:idx], msg_list[idx+1:]
674 return msg_list[:idx], msg_list[idx+1:]
687 else:
675 else:
688 failed = True
676 failed = True
689 for idx,m in enumerate(msg_list):
677 for idx,m in enumerate(msg_list):
690 if m.bytes == DELIM:
678 if m.bytes == DELIM:
691 failed = False
679 failed = False
692 break
680 break
693 if failed:
681 if failed:
694 raise ValueError("DELIM not in msg_list")
682 raise ValueError("DELIM not in msg_list")
695 idents, msg_list = msg_list[:idx], msg_list[idx+1:]
683 idents, msg_list = msg_list[:idx], msg_list[idx+1:]
696 return [m.bytes for m in idents], msg_list
684 return [m.bytes for m in idents], msg_list
697
685
698 def unserialize(self, msg_list, content=True, copy=True):
686 def unserialize(self, msg_list, content=True, copy=True):
699 """Unserialize a msg_list to a nested message dict.
687 """Unserialize a msg_list to a nested message dict.
700
688
701 This is roughly the inverse of serialize. The serialize/unserialize
689 This is roughly the inverse of serialize. The serialize/unserialize
702 methods work with full message lists, whereas pack/unpack work with
690 methods work with full message lists, whereas pack/unpack work with
703 the individual message parts in the message list.
691 the individual message parts in the message list.
704
692
705 Parameters:
693 Parameters:
706 -----------
694 -----------
707 msg_list : list of bytes or Message objects
695 msg_list : list of bytes or Message objects
708 The list of message parts of the form [HMAC,p_header,p_parent,
696 The list of message parts of the form [HMAC,p_header,p_parent,
709 p_content,buffer1,buffer2,...].
697 p_content,buffer1,buffer2,...].
710 content : bool (True)
698 content : bool (True)
711 Whether to unpack the content dict (True), or leave it packed
699 Whether to unpack the content dict (True), or leave it packed
712 (False).
700 (False).
713 copy : bool (True)
701 copy : bool (True)
714 Whether to return the bytes (True), or the non-copying Message
702 Whether to return the bytes (True), or the non-copying Message
715 object in each place (False).
703 object in each place (False).
716
704
717 Returns
705 Returns
718 -------
706 -------
719 msg : dict
707 msg : dict
720 The nested message dict with top-level keys [header, parent_header,
708 The nested message dict with top-level keys [header, parent_header,
721 content, buffers].
709 content, buffers].
722 """
710 """
723 minlen = 4
711 minlen = 4
724 message = {}
712 message = {}
725 if not copy:
713 if not copy:
726 for i in range(minlen):
714 for i in range(minlen):
727 msg_list[i] = msg_list[i].bytes
715 msg_list[i] = msg_list[i].bytes
728 if self.auth is not None:
716 if self.auth is not None:
729 signature = msg_list[0]
717 signature = msg_list[0]
730 if not signature:
718 if not signature:
731 raise ValueError("Unsigned Message")
719 raise ValueError("Unsigned Message")
732 if signature in self.digest_history:
720 if signature in self.digest_history:
733 raise ValueError("Duplicate Signature: %r"%signature)
721 raise ValueError("Duplicate Signature: %r"%signature)
734 self.digest_history.add(signature)
722 self.digest_history.add(signature)
735 check = self.sign(msg_list[1:4])
723 check = self.sign(msg_list[1:4])
736 if not signature == check:
724 if not signature == check:
737 raise ValueError("Invalid Signature: %r"%signature)
725 raise ValueError("Invalid Signature: %r"%signature)
738 if not len(msg_list) >= minlen:
726 if not len(msg_list) >= minlen:
739 raise TypeError("malformed message, must have at least %i elements"%minlen)
727 raise TypeError("malformed message, must have at least %i elements"%minlen)
740 header = self.unpack(msg_list[1])
728 header = self.unpack(msg_list[1])
741 message['header'] = header
729 message['header'] = header
742 message['msg_id'] = header['msg_id']
730 message['msg_id'] = header['msg_id']
743 message['msg_type'] = header['msg_type']
731 message['msg_type'] = header['msg_type']
744 message['parent_header'] = self.unpack(msg_list[2])
732 message['parent_header'] = self.unpack(msg_list[2])
745 if content:
733 if content:
746 message['content'] = self.unpack(msg_list[3])
734 message['content'] = self.unpack(msg_list[3])
747 else:
735 else:
748 message['content'] = msg_list[3]
736 message['content'] = msg_list[3]
749
737
750 message['buffers'] = msg_list[4:]
738 message['buffers'] = msg_list[4:]
751 return message
739 return message
752
740
753 def test_msg2obj():
741 def test_msg2obj():
754 am = dict(x=1)
742 am = dict(x=1)
755 ao = Message(am)
743 ao = Message(am)
756 assert ao.x == am['x']
744 assert ao.x == am['x']
757
745
758 am['y'] = dict(z=1)
746 am['y'] = dict(z=1)
759 ao = Message(am)
747 ao = Message(am)
760 assert ao.y.z == am['y']['z']
748 assert ao.y.z == am['y']['z']
761
749
762 k1, k2 = 'y', 'z'
750 k1, k2 = 'y', 'z'
763 assert ao[k1][k2] == am[k1][k2]
751 assert ao[k1][k2] == am[k1][k2]
764
752
765 am2 = dict(ao)
753 am2 = dict(ao)
766 assert am['x'] == am2['x']
754 assert am['x'] == am2['x']
767 assert am['y']['z'] == am2['y']['z']
755 assert am['y']['z'] == am2['y']['z']
768
756
General Comments 0
You need to be logged in to leave comments. Login now