Mercurial > pydertron
annotate pydertron.py @ 21:cb73bb169b67
Added html docs.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Thu, 10 Sep 2009 16:37:33 -0700 |
parents | d382ca63d43f |
children | 915fdf283ac5 |
rev | line source |
---|---|
18 | 1 # ***** BEGIN LICENSE BLOCK ***** |
2 # Version: MPL 1.1/GPL 2.0/LGPL 2.1 | |
3 # | |
4 # The contents of this file are subject to the Mozilla Public License Version | |
5 # 1.1 (the "License"); you may not use this file except in compliance with | |
6 # the License. You may obtain a copy of the License at | |
7 # http://www.mozilla.org/MPL/ | |
8 # | |
9 # Software distributed under the License is distributed on an "AS IS" basis, | |
10 # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License | |
11 # for the specific language governing rights and limitations under the | |
12 # License. | |
13 # | |
14 # The Original Code is Pydertron. | |
15 # | |
16 # The Initial Developer of the Original Code is Mozilla. | |
17 # Portions created by the Initial Developer are Copyright (C) 2007 | |
18 # the Initial Developer. All Rights Reserved. | |
19 # | |
20 # Contributor(s): | |
21 # Atul Varma <atul@mozilla.com> | |
22 # | |
23 # Alternatively, the contents of this file may be used under the terms of | |
24 # either the GNU General Public License Version 2 or later (the "GPL"), or | |
25 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), | |
26 # in which case the provisions of the GPL or the LGPL are applicable instead | |
27 # of those above. If you wish to allow use of your version of this file only | |
28 # under the terms of either the GPL or the LGPL, and not to allow others to | |
29 # use your version of this file under the terms of the MPL, indicate your | |
30 # decision by deleting the provisions above and replace them with the notice | |
31 # and other provisions required by the GPL or the LGPL. If you do not delete | |
32 # the provisions above, a recipient may use your version of this file under | |
33 # the terms of any one of the MPL, the GPL or the LGPL. | |
34 # | |
35 # ***** END LICENSE BLOCK ***** | |
36 | |
16 | 37 """ |
21 | 38 Pydertron is a high-level wrapper for `Pydermonkey`__ that |
39 provides convenient, secure object wrapping between JS and Python | |
40 space. | |
20 | 41 |
21 | 42 The ``JsSandbox`` class encapsulates a JavaScript runtime, context, global |
43 object, and a simple `SecurableModule`__ implementation that complies | |
44 with the `CommonJS`__ standard. It also provides a high-level bridge between | |
20 | 45 Python and JavaScript so that you don't need to deal with any of the |
46 low-level details of the Pydermonkey API. | |
47 | |
21 | 48 __ http://code.google.com/p/pydermonkey |
49 __ http://wiki.commonjs.org/wiki/CommonJS/Modules/SecurableModules | |
50 __ http://wiki.commonjs.org/wiki/CommonJS | |
51 | |
52 For instance, here we'll create a ``JsSandbox`` whose module root | |
53 points to the ``monkeys`` SecurableModule compliance test over HTTP: | |
20 | 54 |
55 >>> url = ("http://interoperablejs.googlecode.com/svn/trunk/" | |
56 ... "compliance/monkeys/") | |
57 >>> sandbox = JsSandbox(HttpFileSystem(url)) | |
58 | |
21 | 59 This compliance test requires a global ``sys`` object that contains one |
60 method, ``print()``, that takes two arguments. First, we'll create the | |
61 ``print()`` function and prepare it for exposure to JS code: | |
20 | 62 |
63 >>> @jsexposed | |
64 ... def jsprint(message, label): | |
65 ... print message, label | |
66 | |
21 | 67 Note the use of the ``@jsexposed`` decorator: all this does is set |
68 the function's ``__jsexposed__`` attribute to ``True``. This is | |
69 done for security purposes: only Python callables satisfying this | |
70 criteria will be exposed to JavaScript code, to ensure that | |
71 untrusted JS can't accidentally gain access to privileged Python | |
72 functionality. | |
20 | 73 |
74 Creating a JS object can be done like this: | |
75 | |
76 >>> system = sandbox.new_object() | |
77 | |
78 We can now access and set properties on this object via either | |
21 | 79 item or attribute lookup, just like in JavaScript. Because |
80 ``print`` is a reserved word in Python, though, we'll use item | |
81 lookup to set the property here: | |
20 | 82 |
83 >>> system['print'] = jsprint | |
84 | |
21 | 85 Now we tell the sandbox that we want the ``sys`` object to be a |
20 | 86 global: |
87 | |
88 >>> sandbox.set_globals(sys = system) | |
89 | |
90 And finally, we execute the compliance test by running a one-line | |
91 script that imports the 'program' module, like so: | |
92 | |
93 >>> sandbox.run_script("require('program');") | |
94 PASS monkeys permitted pass | |
95 DONE info | |
96 0 | |
97 | |
21 | 98 Note the ``0`` in the last line: this is the return value of |
99 ``sandbox.run_script()``, which returns ``0`` on success, and | |
100 ``-1`` if an exception was raised. For instance, the output of bad | |
20 | 101 code looks like this: |
102 | |
103 >>> sandbox.run_script("(function foo() { bar(); })();", | |
104 ... stderr=sys.stdout) | |
105 Traceback (most recent call last): | |
106 File "<string>", line 1, in <module> | |
107 File "<string>", line 1, in foo | |
108 ReferenceError: bar is not defined | |
109 -1 | |
110 | |
111 Note that the traceback displayed is actually referring to | |
112 JavaScript code: one of Pydertron's helpful conveniences is that | |
113 it makes debugging JS code as much like debugging Python code as | |
114 possible. | |
16 | 115 """ |
116 | |
0 | 117 import sys |
118 import threading | |
119 import traceback | |
120 import weakref | |
121 import types | |
7
2117265e4dfe
Fixed some threading issues.
Atul Varma <varmaa@toolness.com>
parents:
6
diff
changeset
|
122 import atexit |
0 | 123 |
124 import pydermonkey | |
125 | |
126 class ContextWatchdogThread(threading.Thread): | |
127 """ | |
128 Watches active JS contexts and triggers their operation callbacks | |
129 at a regular interval. | |
130 """ | |
131 | |
132 # Default interval, in seconds, that the operation callbacks are | |
133 # triggered at. | |
8
fb0b161542b1
Improved performance of watchdog thread.
Atul Varma <varmaa@toolness.com>
parents:
7
diff
changeset
|
134 DEFAULT_INTERVAL = 0.25 |
0 | 135 |
136 def __init__(self, interval=DEFAULT_INTERVAL): | |
137 threading.Thread.__init__(self) | |
138 self._lock = threading.Lock() | |
139 self._stop = threading.Event() | |
140 self._contexts = [] | |
141 self.interval = interval | |
142 | |
143 def add_context(self, cx): | |
144 self._lock.acquire() | |
145 try: | |
146 self._contexts.append(weakref.ref(cx)) | |
147 finally: | |
148 self._lock.release() | |
149 | |
150 def join(self): | |
151 self._stop.set() | |
152 threading.Thread.join(self) | |
153 | |
154 def run(self): | |
155 while not self._stop.isSet(): | |
156 new_list = [] | |
157 self._lock.acquire() | |
158 try: | |
159 for weakcx in self._contexts: | |
160 cx = weakcx() | |
161 if cx: | |
162 new_list.append(weakcx) | |
163 cx.trigger_operation_callback() | |
7
2117265e4dfe
Fixed some threading issues.
Atul Varma <varmaa@toolness.com>
parents:
6
diff
changeset
|
164 del cx |
0 | 165 self._contexts = new_list |
166 finally: | |
167 self._lock.release() | |
8
fb0b161542b1
Improved performance of watchdog thread.
Atul Varma <varmaa@toolness.com>
parents:
7
diff
changeset
|
168 self._stop.wait(self.interval) |
0 | 169 |
170 # Create a global watchdog. | |
171 watchdog = ContextWatchdogThread() | |
172 watchdog.start() | |
7
2117265e4dfe
Fixed some threading issues.
Atul Varma <varmaa@toolness.com>
parents:
6
diff
changeset
|
173 atexit.register(watchdog.join) |
0 | 174 |
175 class InternalError(BaseException): | |
176 """ | |
177 Represents an error in a JS-wrapped Python function that wasn't | |
178 expected to happen; because it's derived from BaseException, it | |
179 unrolls the whole JS/Python stack so that the error can be | |
180 reported to the outermost calling code. | |
181 """ | |
182 | |
183 def __init__(self): | |
184 BaseException.__init__(self) | |
185 self.exc_info = sys.exc_info() | |
186 | |
187 class SafeJsObjectWrapper(object): | |
188 """ | |
16 | 189 Securely wraps a JS object to behave like any normal Python |
190 object. Like JS objects, though, accessing undefined object | |
191 results merely in pydermonkey.undefined. | |
192 | |
193 Object properties may be accessed either via attribute or | |
194 item-based lookup. | |
0 | 195 """ |
196 | |
197 __slots__ = ['_jsobject', '_sandbox', '_this'] | |
198 | |
199 def __init__(self, sandbox, jsobject, this): | |
200 if not isinstance(jsobject, pydermonkey.Object): | |
201 raise TypeError("Cannot wrap '%s' object" % | |
202 type(jsobject).__name__) | |
203 object.__setattr__(self, '_sandbox', sandbox) | |
204 object.__setattr__(self, '_jsobject', jsobject) | |
205 object.__setattr__(self, '_this', this) | |
206 | |
207 @property | |
208 def wrapped_jsobject(self): | |
209 return self._jsobject | |
210 | |
211 def _wrap_to_python(self, jsvalue): | |
212 return self._sandbox.wrap_jsobject(jsvalue, self._jsobject) | |
213 | |
214 def _wrap_to_js(self, value): | |
215 return self._sandbox.wrap_pyobject(value) | |
216 | |
217 def __eq__(self, other): | |
218 if isinstance(other, SafeJsObjectWrapper): | |
219 return self._jsobject == other._jsobject | |
220 else: | |
221 return False | |
222 | |
223 def __str__(self): | |
224 return self.toString() | |
225 | |
226 def __unicode__(self): | |
227 return self.toString() | |
228 | |
229 def __setitem__(self, item, value): | |
230 self.__setattr__(item, value) | |
231 | |
232 def __setattr__(self, name, value): | |
233 cx = self._sandbox.cx | |
234 jsobject = self._jsobject | |
235 | |
236 cx.define_property(jsobject, name, | |
237 self._wrap_to_js(value)) | |
238 | |
239 def __getitem__(self, item): | |
240 return self.__getattr__(item) | |
241 | |
242 def __getattr__(self, name): | |
243 cx = self._sandbox.cx | |
244 jsobject = self._jsobject | |
245 | |
246 return self._wrap_to_python(cx.get_property(jsobject, name)) | |
247 | |
248 def __contains__(self, item): | |
249 cx = self._sandbox.cx | |
250 jsobject = self._jsobject | |
251 | |
252 return cx.has_property(jsobject, item) | |
253 | |
254 def __iter__(self): | |
255 cx = self._sandbox.cx | |
256 jsobject = self._jsobject | |
257 | |
258 properties = cx.enumerate(jsobject) | |
259 for property in properties: | |
260 yield property | |
261 | |
262 class SafeJsFunctionWrapper(SafeJsObjectWrapper): | |
263 """ | |
264 Securely wraps a JS function to behave like any normal Python object. | |
265 """ | |
266 | |
267 def __init__(self, sandbox, jsfunction, this): | |
268 if not isinstance(jsfunction, pydermonkey.Function): | |
269 raise TypeError("Cannot wrap '%s' object" % | |
270 type(jsobject).__name__) | |
271 SafeJsObjectWrapper.__init__(self, sandbox, jsfunction, this) | |
272 | |
273 def __call__(self, *args): | |
274 cx = self._sandbox.cx | |
275 jsobject = self._jsobject | |
276 this = self._this | |
277 | |
278 arglist = [] | |
279 for arg in args: | |
280 arglist.append(self._wrap_to_js(arg)) | |
281 | |
282 obj = cx.call_function(this, jsobject, tuple(arglist)) | |
283 return self._wrap_to_python(obj) | |
284 | |
11 | 285 def format_stack(js_stack, open=open): |
0 | 286 """ |
287 Returns a formatted Python-esque stack traceback of the given | |
288 JS stack. | |
289 """ | |
290 | |
291 STACK_LINE =" File \"%(filename)s\", line %(lineno)d, in %(name)s" | |
292 | |
293 lines = [] | |
294 while js_stack: | |
295 script = js_stack['script'] | |
296 function = js_stack['function'] | |
297 if script: | |
298 frameinfo = dict(filename = script.filename, | |
299 lineno = js_stack['lineno'], | |
300 name = '<module>') | |
301 elif function and not function.is_python: | |
302 frameinfo = dict(filename = function.filename, | |
303 lineno = js_stack['lineno'], | |
304 name = function.name) | |
305 else: | |
306 frameinfo = None | |
307 if frameinfo: | |
308 lines.insert(0, STACK_LINE % frameinfo) | |
309 try: | |
310 filelines = open(frameinfo['filename']).readlines() | |
311 line = filelines[frameinfo['lineno'] - 1].strip() | |
312 lines.insert(1, " %s" % line) | |
313 except Exception: | |
314 pass | |
315 js_stack = js_stack['caller'] | |
316 lines.insert(0, "Traceback (most recent call last):") | |
317 return '\n'.join(lines) | |
318 | |
319 def jsexposed(name=None, on=None): | |
320 """ | |
321 Decorator used to expose the decorated function or method to | |
322 untrusted JS. | |
323 | |
324 'name' is an optional alternative name for the function. | |
325 | |
326 'on' is an optional SafeJsObjectWrapper that the function can be | |
327 automatically attached as a property to. | |
328 """ | |
329 | |
330 if callable(name): | |
331 func = name | |
332 func.__jsexposed__ = True | |
333 return func | |
334 | |
335 def make_exposed(func): | |
336 if name: | |
337 func.__name__ = name | |
338 func.__jsexposed__ = True | |
339 if on: | |
340 on[func.__name__] = func | |
341 return func | |
342 return make_exposed | |
343 | |
344 class JsExposedObject(object): | |
345 """ | |
346 Trivial base/mixin class for any Python classes that choose to | |
347 expose themselves to JS code. | |
348 """ | |
349 | |
350 pass | |
351 | |
352 class JsSandbox(object): | |
353 """ | |
354 A JS runtime and associated functionality capable of securely | |
355 loading and executing scripts. | |
356 """ | |
357 | |
19 | 358 def __init__(self, fs, watchdog=watchdog, opcb=None): |
0 | 359 rt = pydermonkey.Runtime() |
360 cx = rt.new_context() | |
2
b6f9d743a2b5
Refined require() implementation.
Atul Varma <varmaa@toolness.com>
parents:
1
diff
changeset
|
361 root_proto = cx.new_object() |
b6f9d743a2b5
Refined require() implementation.
Atul Varma <varmaa@toolness.com>
parents:
1
diff
changeset
|
362 cx.init_standard_classes(root_proto) |
b6f9d743a2b5
Refined require() implementation.
Atul Varma <varmaa@toolness.com>
parents:
1
diff
changeset
|
363 root = cx.new_object(None, root_proto) |
0 | 364 |
365 cx.set_operation_callback(self._opcb) | |
366 cx.set_throw_hook(self._throwhook) | |
367 watchdog.add_context(cx) | |
368 | |
11 | 369 self.fs = fs |
19 | 370 self.opcb = opcb |
0 | 371 self.rt = rt |
372 self.cx = cx | |
373 self.curr_exc = None | |
374 self.py_stack = None | |
375 self.js_stack = None | |
11 | 376 self.__modules = {} |
0 | 377 self.__py_to_js = {} |
378 self.__type_protos = {} | |
11 | 379 self.__globals = {} |
380 self.__root_proto = root_proto | |
0 | 381 self.root = self.wrap_jsobject(root, root) |
382 | |
11 | 383 def set_globals(self, **globals): |
16 | 384 """ |
385 Sets the global properties for the root object and all global | |
386 scopes (e.g., SecurableModules). This should be called before | |
387 any scripts are executed. | |
388 """ | |
389 | |
11 | 390 self.__globals.update(globals) |
391 self._install_globals(self.root) | |
392 | |
0 | 393 def finish(self): |
394 """ | |
395 Cleans up all resources used by the sandbox, breaking any reference | |
396 cycles created due to issue #2 in pydermonkey: | |
397 | |
398 http://code.google.com/p/pydermonkey/issues/detail?id=2 | |
399 """ | |
400 | |
401 for jsobj in self.__py_to_js.values(): | |
402 self.cx.clear_object_private(jsobj) | |
403 del self.__py_to_js | |
404 del self.__type_protos | |
405 del self.curr_exc | |
406 del self.py_stack | |
407 del self.js_stack | |
408 del self.cx | |
409 del self.rt | |
410 | |
411 def _opcb(self, cx): | |
19 | 412 # Note that if a keyboard interrupt was triggered, |
0 | 413 # it'll get raised here automatically. |
19 | 414 if self.opcb: |
415 self.opcb() | |
0 | 416 |
417 def _throwhook(self, cx): | |
418 curr_exc = cx.get_pending_exception() | |
419 if self.curr_exc != curr_exc: | |
420 self.curr_exc = curr_exc | |
421 self.py_stack = traceback.extract_stack() | |
422 self.js_stack = cx.get_stack() | |
423 | |
424 def __wrap_pycallable(self, func, pyproto=None): | |
425 if func in self.__py_to_js: | |
426 return self.__py_to_js[func] | |
427 | |
428 if hasattr(func, '__name__'): | |
429 name = func.__name__ | |
430 else: | |
431 name = "" | |
432 | |
433 if pyproto: | |
434 def wrapper(func_cx, this, args): | |
435 try: | |
436 arglist = [] | |
437 for arg in args: | |
438 arglist.append(self.wrap_jsobject(arg)) | |
439 instance = func_cx.get_object_private(this) | |
440 if instance is None or not isinstance(instance, pyproto): | |
441 raise pydermonkey.error("Method type mismatch") | |
442 | |
443 # TODO: Fill in extra required params with | |
444 # pymonkey.undefined? or automatically throw an | |
445 # exception to calling js code? | |
446 return self.wrap_pyobject(func(instance, *arglist)) | |
447 except pydermonkey.error: | |
448 raise | |
449 except Exception: | |
450 raise InternalError() | |
451 else: | |
452 def wrapper(func_cx, this, args): | |
453 try: | |
454 arglist = [] | |
455 for arg in args: | |
456 arglist.append(self.wrap_jsobject(arg)) | |
457 | |
458 # TODO: Fill in extra required params with | |
459 # pymonkey.undefined? or automatically throw an | |
460 # exception to calling js code? | |
461 return self.wrap_pyobject(func(*arglist)) | |
462 except pydermonkey.error: | |
463 raise | |
464 except Exception: | |
465 raise InternalError() | |
466 wrapper.wrapped_pyobject = func | |
467 wrapper.__name__ = name | |
468 | |
469 jsfunc = self.cx.new_function(wrapper, name) | |
470 self.__py_to_js[func] = jsfunc | |
471 | |
472 return jsfunc | |
473 | |
474 def __wrap_pyinstance(self, value): | |
475 pyproto = type(value) | |
476 if pyproto not in self.__type_protos: | |
477 jsproto = self.cx.new_object() | |
478 if hasattr(pyproto, '__jsprops__'): | |
479 define_getter = self.cx.get_property(jsproto, | |
480 '__defineGetter__') | |
481 define_setter = self.cx.get_property(jsproto, | |
482 '__defineSetter__') | |
483 for name in pyproto.__jsprops__: | |
484 prop = getattr(pyproto, name) | |
485 if not type(prop) == property: | |
486 raise TypeError("Expected attribute '%s' to " | |
487 "be a property" % name) | |
488 getter = None | |
489 setter = None | |
490 if prop.fget: | |
491 getter = self.__wrap_pycallable(prop.fget, | |
492 pyproto) | |
493 if prop.fset: | |
494 setter = self.__wrap_pycallable(prop.fset, | |
495 pyproto) | |
496 if getter: | |
497 self.cx.call_function(jsproto, | |
498 define_getter, | |
499 (name, getter)) | |
500 if setter: | |
501 self.cx.call_function(jsproto, | |
502 define_setter, | |
503 (name, setter,)) | |
504 for name in dir(pyproto): | |
505 attr = getattr(pyproto, name) | |
506 if (isinstance(attr, types.UnboundMethodType) and | |
507 hasattr(attr, '__jsexposed__') and | |
508 attr.__jsexposed__): | |
509 jsmethod = self.__wrap_pycallable(attr, pyproto) | |
510 self.cx.define_property(jsproto, name, jsmethod) | |
511 self.__type_protos[pyproto] = jsproto | |
512 return self.cx.new_object(value, self.__type_protos[pyproto]) | |
513 | |
514 def wrap_pyobject(self, value): | |
515 """ | |
516 Wraps the given Python object for export to untrusted JS. | |
517 | |
518 If the Python object isn't of a type that can be exposed to JS, | |
519 a TypeError is raised. | |
520 """ | |
521 | |
522 if (isinstance(value, (int, basestring, float, bool)) or | |
523 value is pydermonkey.undefined or | |
524 value is None): | |
525 return value | |
526 if isinstance(value, SafeJsObjectWrapper): | |
527 # It's already wrapped, just unwrap it. | |
528 return value.wrapped_jsobject | |
529 elif callable(value): | |
530 if not (hasattr(value, '__jsexposed__') and | |
531 value.__jsexposed__): | |
532 raise ValueError("Callable isn't configured for exposure " | |
533 "to untrusted JS code") | |
534 return self.__wrap_pycallable(value) | |
535 elif isinstance(value, JsExposedObject): | |
536 return self.__wrap_pyinstance(value) | |
537 else: | |
538 raise TypeError("Can't expose objects of type '%s' to JS." % | |
539 type(value).__name__) | |
540 | |
541 def wrap_jsobject(self, jsvalue, this=None): | |
542 """ | |
543 Wraps the given pydermonkey.Object for import to trusted | |
544 Python code. If the type is just a primitive, it's simply | |
545 returned, since no wrapping is needed. | |
546 """ | |
547 | |
548 if this is None: | |
549 this = self.root.wrapped_jsobject | |
550 if isinstance(jsvalue, pydermonkey.Function): | |
551 if jsvalue.is_python: | |
552 # It's a Python function, just unwrap it. | |
553 return self.cx.get_object_private(jsvalue).wrapped_pyobject | |
554 return SafeJsFunctionWrapper(self, jsvalue, this) | |
555 elif isinstance(jsvalue, pydermonkey.Object): | |
556 # It's a wrapped Python object instance, just unwrap it. | |
557 instance = self.cx.get_object_private(jsvalue) | |
558 if instance: | |
559 if not isinstance(instance, JsExposedObject): | |
560 raise AssertionError("Object private is not of type " | |
561 "JsExposedObject") | |
562 return instance | |
563 else: | |
564 return SafeJsObjectWrapper(self, jsvalue, this) | |
565 else: | |
566 # It's a primitive value. | |
567 return jsvalue | |
568 | |
569 def new_array(self, *contents): | |
16 | 570 """ |
571 Creates a new JavaScript array with the given contents and | |
572 returns a wrapper for it. | |
573 """ | |
574 | |
0 | 575 array = self.wrap_jsobject(self.cx.new_array_object()) |
576 for item in contents: | |
577 array.push(item) | |
578 return array | |
579 | |
580 def new_object(self, **contents): | |
16 | 581 """ |
582 Creates a new JavaScript object with the given properties and | |
583 returns a wrapper for it. | |
584 """ | |
585 | |
0 | 586 obj = self.wrap_jsobject(self.cx.new_object()) |
587 for name in contents: | |
588 obj[name] = contents[name] | |
589 return obj | |
590 | |
11 | 591 def get_calling_script(self): |
16 | 592 """ |
593 Returns the filename of the current stack's most recent | |
594 JavaScript caller. | |
595 """ | |
596 | |
11 | 597 frame = self.cx.get_stack()['caller'] |
598 curr_script = None | |
599 while frame and curr_script is None: | |
600 if frame['function'] and frame['function'].filename: | |
601 curr_script = frame['function'].filename | |
602 elif frame['script']: | |
603 curr_script = frame['script'].filename | |
604 frame = frame['caller'] | |
605 | |
606 if curr_script is None: | |
607 raise RuntimeError("Can't find calling script") | |
608 return curr_script | |
609 | |
610 def _install_globals(self, object): | |
611 for name in self.__globals: | |
612 object[name] = self.__globals[name] | |
613 object['require'] = self._require | |
614 | |
615 @jsexposed(name='require') | |
616 def _require(self, path): | |
16 | 617 """ |
618 Implementation for the global require() function, implemented | |
619 as per the CommonJS SecurableModule specification: | |
620 | |
621 http://wiki.commonjs.org/wiki/CommonJS/Modules/SecurableModules | |
622 """ | |
623 | |
11 | 624 filename = self.fs.find_module(self.get_calling_script(), path) |
625 if not filename: | |
626 raise pydermonkey.error('Module not found: %s' % path) | |
627 if not filename in self.__modules: | |
628 cx = self.cx | |
629 module = cx.new_object(None, self.__root_proto) | |
630 cx.init_standard_classes(module) | |
631 exports = cx.new_object() | |
632 cx.define_property(module, 'exports', exports) | |
633 self._install_globals(self.wrap_jsobject(module)) | |
634 self.__modules[filename] = self.wrap_jsobject(exports) | |
635 contents = self.fs.open(filename).read() | |
636 cx.evaluate_script(module, contents, filename, 1) | |
637 return self.__modules[filename] | |
0 | 638 |
6
97adec8c8127
Now passing all CommonJS SecurableModule compliance tests.
Atul Varma <varmaa@toolness.com>
parents:
5
diff
changeset
|
639 def run_script(self, contents, filename='<string>', lineno=1, |
20 | 640 callback=None, stderr=None): |
0 | 641 """ |
642 Runs the given JS script, returning 0 on success, -1 on failure. | |
643 """ | |
644 | |
20 | 645 if stderr is None: |
646 stderr = sys.stderr | |
647 | |
0 | 648 retval = -1 |
649 cx = self.cx | |
650 try: | |
651 result = cx.evaluate_script(self.root.wrapped_jsobject, | |
6
97adec8c8127
Now passing all CommonJS SecurableModule compliance tests.
Atul Varma <varmaa@toolness.com>
parents:
5
diff
changeset
|
652 contents, filename, lineno) |
0 | 653 if callback: |
654 callback(self.wrap_jsobject(result)) | |
655 retval = 0 | |
656 except pydermonkey.error, e: | |
11 | 657 params = dict( |
658 stack_trace = format_stack(self.js_stack, self.fs.open), | |
659 error = e.args[1] | |
660 ) | |
661 stderr.write("%(stack_trace)s\n%(error)s\n" % params) | |
0 | 662 except InternalError, e: |
11 | 663 stderr.write("An internal error occurred.\n") |
664 traceback.print_tb(e.exc_info[2], None, stderr) | |
665 stderr.write("%s\n" % e.exc_info[1]) | |
0 | 666 return retval |
1
ab09b8a10876
Added trivial half-baked implementation of securable modules.
Atul Varma <varmaa@toolness.com>
parents:
0
diff
changeset
|
667 |
14 | 668 class HttpFileSystem(object): |
16 | 669 """ |
670 File system through which all resources are loaded over HTTP. | |
671 """ | |
672 | |
14 | 673 def __init__(self, base_url): |
674 self.base_url = base_url | |
675 | |
676 def find_module(self, curr_url, path): | |
677 import urlparse | |
678 | |
679 if path.startswith("."): | |
680 base_url = curr_url | |
681 else: | |
682 base_url = self.base_url | |
683 | |
684 url = "%s.js" % urlparse.urljoin(base_url, path) | |
685 if not url.startswith(self.base_url): | |
686 return None | |
687 return url | |
688 | |
689 def open(self, url): | |
690 import urllib | |
691 | |
692 return urllib.urlopen(url) | |
693 | |
16 | 694 class LocalFileSystem(object): |
695 """ | |
696 File system through which all resources are loaded over the local | |
697 filesystem. | |
698 """ | |
699 | |
11 | 700 def __init__(self, root_dir): |
3
14d8d73774d7
Refactored securable module loader.
Atul Varma <varmaa@toolness.com>
parents:
2
diff
changeset
|
701 self.root_dir = root_dir |
2
b6f9d743a2b5
Refined require() implementation.
Atul Varma <varmaa@toolness.com>
parents:
1
diff
changeset
|
702 |
11 | 703 def find_module(self, curr_script, path): |
14 | 704 import os |
705 | |
11 | 706 if path.startswith("."): |
707 base_dir = os.path.dirname(curr_script) | |
708 else: | |
709 base_dir = self.root_dir | |
2
b6f9d743a2b5
Refined require() implementation.
Atul Varma <varmaa@toolness.com>
parents:
1
diff
changeset
|
710 |
11 | 711 ospath = path.replace('/', os.path.sep) |
712 filename = os.path.join(base_dir, "%s.js" % ospath) | |
713 filename = os.path.normpath(filename) | |
714 if (filename.startswith(self.root_dir) and | |
715 (os.path.exists(filename) and | |
716 not os.path.isdir(filename))): | |
717 return filename | |
718 else: | |
719 return None | |
3
14d8d73774d7
Refactored securable module loader.
Atul Varma <varmaa@toolness.com>
parents:
2
diff
changeset
|
720 |
11 | 721 def open(self, filename): |
722 return open(filename, 'r') |