comparison pydershell/pydershell.py @ 19:057260102960

Made wrapping of python objects much nicer.
author Atul Varma <varmaa@toolness.com>
date Mon, 07 Sep 2009 12:57:20 -0700
parents 69e5523ebdc6
children 8dd18f864351
comparison
equal deleted inserted replaced
18:69e5523ebdc6 19:057260102960
69 class SafeJsObjectWrapper(object): 69 class SafeJsObjectWrapper(object):
70 """ 70 """
71 Securely wraps a JS object to behave like any normal Python object. 71 Securely wraps a JS object to behave like any normal Python object.
72 """ 72 """
73 73
74 __slots__ = ['_jsobject', '_cx', '_this'] 74 __slots__ = ['_jsobject', '_sandbox', '_this']
75 75
76 def __init__(self, cx, jsobject, this): 76 def __init__(self, sandbox, jsobject, this):
77 if not isinstance(jsobject, pydermonkey.Object): 77 if not isinstance(jsobject, pydermonkey.Object):
78 raise TypeError("Cannot wrap '%s' object" % 78 raise TypeError("Cannot wrap '%s' object" %
79 type(jsobject).__name__) 79 type(jsobject).__name__)
80 object.__setattr__(self, '_sandbox', sandbox)
80 object.__setattr__(self, '_jsobject', jsobject) 81 object.__setattr__(self, '_jsobject', jsobject)
81 object.__setattr__(self, '_cx', cx)
82 object.__setattr__(self, '_this', this) 82 object.__setattr__(self, '_this', this)
83 83
84 @staticmethod 84 @property
85 def wrap(cx, jsvalue, this): 85 def wrapped_jsobject(self):
86 if isinstance(jsvalue, pydermonkey.Function): 86 return self._jsobject
87 return SafeJsFunctionWrapper(cx, jsvalue, this)
88 elif isinstance(jsvalue, pydermonkey.Object):
89 return SafeJsObjectWrapper(cx, jsvalue, this)
90 else:
91 # It's a primitive value.
92 return jsvalue
93 87
94 def _wrap_to_python(self, jsvalue): 88 def _wrap_to_python(self, jsvalue):
95 return self.wrap(self._cx, jsvalue, self._jsobject) 89 return self._sandbox.wrap_jsobject(jsvalue, self._jsobject)
96 90
97 def _wrap_to_js(self, value): 91 def _wrap_to_js(self, value):
98 # TODO: Add support for wrapping non-primitive python objects. 92 return self._sandbox.wrap_pyobject(value)
99 return value
100 93
101 def __eq__(self, other): 94 def __eq__(self, other):
102 if isinstance(other, SafeJsObjectWrapper): 95 if isinstance(other, SafeJsObjectWrapper):
103 return self._jsobject == other._jsobject 96 return self._jsobject == other._jsobject
104 else: 97 else:
112 105
113 def __setitem__(self, item, value): 106 def __setitem__(self, item, value):
114 self.__setattr__(item, value) 107 self.__setattr__(item, value)
115 108
116 def __setattr__(self, name, value): 109 def __setattr__(self, name, value):
117 cx = self._cx 110 cx = self._sandbox.cx
118 jsobject = self._jsobject 111 jsobject = self._jsobject
119 112
120 cx.define_property(jsobject, name, 113 cx.define_property(jsobject, name,
121 self._wrap_to_js(value)) 114 self._wrap_to_js(value))
122 115
123 def __getitem__(self, item): 116 def __getitem__(self, item):
124 return self.__getattr__(item) 117 return self.__getattr__(item)
125 118
126 def __getattr__(self, name): 119 def __getattr__(self, name):
127 cx = self._cx 120 cx = self._sandbox.cx
128 jsobject = self._jsobject 121 jsobject = self._jsobject
129 122
130 return self._wrap_to_python(cx.get_property(jsobject, name)) 123 return self._wrap_to_python(cx.get_property(jsobject, name))
131 124
132 def __contains__(self, item): 125 def __contains__(self, item):
133 cx = self._cx 126 cx = self._sandbox.cx
134 jsobject = self._jsobject 127 jsobject = self._jsobject
135 128
136 return cx.has_property(jsobject, item) 129 return cx.has_property(jsobject, item)
137 130
138 def __iter__(self): 131 def __iter__(self):
139 cx = self._cx 132 cx = self._sandbox.cx
140 jsobject = self._jsobject 133 jsobject = self._jsobject
141 134
142 properties = cx.enumerate(jsobject) 135 properties = cx.enumerate(jsobject)
143 for property in properties: 136 for property in properties:
144 yield property 137 yield property
146 class SafeJsFunctionWrapper(SafeJsObjectWrapper): 139 class SafeJsFunctionWrapper(SafeJsObjectWrapper):
147 """ 140 """
148 Securely wraps a JS function to behave like any normal Python object. 141 Securely wraps a JS function to behave like any normal Python object.
149 """ 142 """
150 143
151 def __init__(self, cx, jsfunction, this): 144 def __init__(self, sandbox, jsfunction, this):
152 if not isinstance(jsfunction, pydermonkey.Function): 145 if not isinstance(jsfunction, pydermonkey.Function):
153 raise TypeError("Cannot wrap '%s' object" % 146 raise TypeError("Cannot wrap '%s' object" %
154 type(jsobject).__name__) 147 type(jsobject).__name__)
155 SafeJsObjectWrapper.__init__(self, cx, jsfunction, this) 148 SafeJsObjectWrapper.__init__(self, sandbox, jsfunction, this)
156 149
157 def __call__(self, *args): 150 def __call__(self, *args):
158 cx = self._cx 151 cx = self._sandbox.cx
159 jsobject = self._jsobject 152 jsobject = self._jsobject
160 this = self._this 153 this = self._this
161 154
162 obj = cx.call_function(this, jsobject, 155 arglist = []
163 self._wrap_to_js(args)) 156 for arg in args:
157 arglist.append(self._wrap_to_js(arg))
158
159 obj = cx.call_function(this, jsobject, tuple(arglist))
164 return self._wrap_to_python(obj) 160 return self._wrap_to_python(obj)
165
166 def safejsfunc(cx, on_obj, name=None):
167 """
168 Exposes the decorated Python function on the given JS object.
169
170 Any unexpected exceptions raised by the function will be
171 re-raised as InternalError exceptions.
172 """
173
174 def make_wrapper(func):
175 if name is None:
176 func_name = func.__name__
177 else:
178 func_name = name
179 def wrapper(func_cx, this, args):
180 try:
181 return func(func_cx, this, args)
182 except pydermonkey.error:
183 raise
184 except Exception:
185 raise InternalError()
186 cx.define_property(
187 on_obj,
188 func_name,
189 cx.new_function(wrapper, func_name)
190 )
191 return func
192 return make_wrapper
193 161
194 def format_stack(js_stack): 162 def format_stack(js_stack):
195 """ 163 """
196 Returns a formatted Python-esque stack traceback of the given 164 Returns a formatted Python-esque stack traceback of the given
197 JS stack. 165 JS stack.
223 pass 191 pass
224 js_stack = js_stack['caller'] 192 js_stack = js_stack['caller']
225 lines.insert(0, "Traceback (most recent call last):") 193 lines.insert(0, "Traceback (most recent call last):")
226 return '\n'.join(lines) 194 return '\n'.join(lines)
227 195
228 class JSRuntime(object): 196 class JSSandbox(object):
229 """ 197 """
230 A JS runtime capable of loading and executing scripts. 198 A JS runtime and associated functionality capable of securely
199 loading and executing scripts.
231 """ 200 """
232 201
233 def __init__(self, watchdog=watchdog): 202 def __init__(self, watchdog=watchdog):
234 rt = pydermonkey.Runtime() 203 rt = pydermonkey.Runtime()
235 cx = rt.new_context() 204 cx = rt.new_context()
236 globalobj = cx.new_object() 205 root = cx.new_object()
237 cx.init_standard_classes(globalobj) 206 cx.init_standard_classes(root)
238
239 @safejsfunc(cx, globalobj)
240 def bar(cx, this, args):
241 obj = SafeJsObjectWrapper.wrap(cx, args[0], this)
242 print obj.bar()
243
244 @safejsfunc(cx, globalobj)
245 def foo(cx, this, args):
246 return cx.call_function(this, args[0], ())
247
248 @safejsfunc(cx, globalobj, 'print')
249 def jsprint(cx, this, args):
250 if len(args) > 0:
251 print args[0]
252 207
253 cx.set_operation_callback(self._opcb) 208 cx.set_operation_callback(self._opcb)
254 cx.set_throw_hook(self._throwhook) 209 cx.set_throw_hook(self._throwhook)
255 watchdog.add_context(cx) 210 watchdog.add_context(cx)
256 211
257 self.rt = rt 212 self.rt = rt
258 self.cx = cx 213 self.cx = cx
259 self.globalobj = globalobj
260 self.curr_exc = None 214 self.curr_exc = None
261 self.py_stack = None 215 self.py_stack = None
262 self.js_stack = None 216 self.js_stack = None
217 self.__py_to_js = {}
218 self.root = self.wrap_jsobject(root, root)
219
220 def finish(self):
221 for jsobj in self.__py_to_js.values():
222 self.cx.clear_object_private(jsobj)
223 del self.__py_to_js
224 del self.curr_exc
225 del self.py_stack
226 del self.js_stack
227 del self.cx
228 del self.rt
263 229
264 def _opcb(self, cx): 230 def _opcb(self, cx):
265 # Don't do anything; if a keyboard interrupt was triggered, 231 # Don't do anything; if a keyboard interrupt was triggered,
266 # it'll get raised here automatically. 232 # it'll get raised here automatically.
267 pass 233 pass
271 if self.curr_exc != curr_exc: 237 if self.curr_exc != curr_exc:
272 self.curr_exc = curr_exc 238 self.curr_exc = curr_exc
273 self.py_stack = traceback.extract_stack() 239 self.py_stack = traceback.extract_stack()
274 self.js_stack = cx.get_stack() 240 self.js_stack = cx.get_stack()
275 241
242 def __wrap_pycallable(self, func):
243 if func in self.__py_to_js:
244 return self.__py_to_js[func]
245
246 if hasattr(func, '__name__'):
247 name = func.__name__
248 else:
249 name = ""
250
251 def wrapper(func_cx, this, args):
252 try:
253 arglist = []
254 for arg in args:
255 arglist.append(self.wrap_jsobject(arg))
256 # TODO: Fill in extra required params with pymonkey.undefined?
257 # or automatically throw an exception to calling js code?
258 return func(*arglist)
259 except pydermonkey.error:
260 raise
261 except Exception:
262 raise InternalError()
263 wrapper.wrapped_pyobject = func
264 wrapper.__name__ = name
265
266 jsfunc = self.cx.new_function(wrapper, name)
267 self.__py_to_js[func] = jsfunc
268
269 return jsfunc
270
271 def wrap_pyobject(self, value):
272 if isinstance(value, SafeJsObjectWrapper):
273 # It's already wrapped, just unwrap it.
274 return value.wrapped_jsobject
275 elif callable(value):
276 return self.__wrap_pycallable(value)
277 elif isinstance(value, object):
278 raise NotImplementedError("TODO")
279 else:
280 # Here's hoping it's a primitive value.
281 return value
282
283 def wrap_jsobject(self, jsvalue, this=None):
284 if this is None:
285 this = self.root.wrapped_jsobject
286 if isinstance(jsvalue, pydermonkey.Function):
287 if jsvalue.is_python:
288 # It's a Python function, just unwrap it.
289 return self.cx.get_object_private(jsvalue).wrapped_pyobject
290 return SafeJsFunctionWrapper(self, jsvalue, this)
291 elif isinstance(jsvalue, pydermonkey.Object):
292 return SafeJsObjectWrapper(self, jsvalue, this)
293 else:
294 # It's a primitive value.
295 return jsvalue
296
276 def run_script(self, filename): 297 def run_script(self, filename):
277 contents = open(filename).read() 298 contents = open(filename).read()
278 cx = self.cx 299 cx = self.cx
279 try: 300 try:
280 cx.evaluate_script(self.globalobj, contents, 301 cx.evaluate_script(self.root.wrapped_jsobject, contents,
281 filename, 1) 302 filename, 1)
282 except pydermonkey.error, e: 303 except pydermonkey.error, e:
283 print format_stack(self.js_stack) 304 print format_stack(self.js_stack)
284 print e.args[1] 305 print e.args[1]
285 except InternalError, e: 306 except InternalError, e:
286 print "An internal error occurred." 307 print "An internal error occurred."
287 traceback.print_tb(e.exc_info[2]) 308 traceback.print_tb(e.exc_info[2])
288 print e.exc_info[1] 309 print e.exc_info[1]
289 310
290 if __name__ == '__main__': 311 if __name__ == '__main__':
291 runtime = JSRuntime() 312 sandbox = JSSandbox()
292 runtime.run_script('test.js') 313
293 del runtime 314 def bar(obj):
315 print obj.bar()
316 sandbox.root.bar = bar
317
318 def foo(callback):
319 return callback()
320 sandbox.root.foo = foo
321
322 def jsprint(string):
323 print string
324 sandbox.root['print'] = jsprint
325
326 sandbox.run_script('test.js')
327
328 sandbox.finish()
329 del sandbox
294 330
295 import gc 331 import gc
296 gc.collect() 332 gc.collect()
297 if pydermonkey.get_debug_info()['runtime_count']: 333 if pydermonkey.get_debug_info()['runtime_count']:
298 print "WARNING: JS runtime was not destroyed." 334 print "WARNING: JS runtime was not destroyed."