diff pydershell/pydershell.py @ 12:f024e41d0fb9

More refactoring, a bit more documentation.
author Atul Varma <varmaa@toolness.com>
date Sun, 06 Sep 2009 14:25:10 -0700
parents 74f27983a350
children 2a3313bfe574
line wrap: on
line diff
--- a/pydershell/pydershell.py	Sun Sep 06 20:12:48 2009 +0000
+++ b/pydershell/pydershell.py	Sun Sep 06 14:25:10 2009 -0700
@@ -4,19 +4,74 @@
 import time
 import threading
 import traceback
+import weakref
+
 import pydermonkey
 
+class ContextWatchdogThread(threading.Thread):
+    """
+    Watches active JS contexts and triggers their operation callbacks
+    at a regular interval.
+    """
+
+    def __init__(self, interval=0.25):
+        threading.Thread.__init__(self)
+        self._lock = threading.Lock()
+        self._stop = threading.Event()
+        self._contexts = []
+        self.interval = interval
+        self.setDaemon(True)
+
+    def add_context(self, cx):
+        self._lock.acquire()
+        try:
+            self._contexts.append(weakref.ref(cx))
+        finally:
+            self._lock.release()
+
+    def join(self):
+        self._stop.set()
+        threading.Thread.join(self)
+
+    def run(self):
+        while not self._stop.isSet():
+            time.sleep(self.interval)
+            new_list = []
+            self._lock.acquire()
+            try:
+                for weakcx in self._contexts:
+                    cx = weakcx()
+                    if cx:
+                        new_list.append(weakcx)
+                        cx.trigger_operation_callback()
+                self._contexts = new_list
+            finally:
+                self._lock.release()
+
+# Create a global watchdog.
+watchdog = ContextWatchdogThread()
+watchdog.start()
+
 class InternalError(BaseException):
+    """
+    Represents an error in a JS-wrapped Python function that wasn't
+    expected to happen; because it's derived from BaseException, it
+    unrolls the whole JS/Python stack so that the error can be
+    reported to the outermost calling code.
+    """
+
     def __init__(self):
         BaseException.__init__(self)
         self.exc_info = sys.exc_info()
 
-rt = pydermonkey.Runtime()
-cx = rt.new_context()
-globalobj = cx.new_object()
-cx.init_standard_classes(globalobj)
+def safejsfunc(cx, on_obj, name=None):
+    """
+    Exposes the decorated Python function on the given JS object.
 
-def safejsfunc(cx, on_obj, name=None):
+    Any unexpected exceptions raised by the function will be
+    re-raised as InternalError exceptions.
+    """
+
     def make_wrapper(func):
         if name is None:
             func_name = func.__name__
@@ -37,77 +92,91 @@
         return func
     return make_wrapper
 
-@safejsfunc(cx, globalobj)
-def foo(cx, this, args):
-    return cx.call_function(this, args[0], ())
-
-@safejsfunc(cx, globalobj, 'print')
-def jsprint(cx, this, args):
-    if len(args) > 0:
-        print args[0]
-
-def opcb(cx):
-    # Don't do anything; if a keyboard interrupt was triggered,
-    # it'll get raised here automatically.
-    pass
-
-cx.set_operation_callback(opcb)
-
-class State(object):
-    def __init__(self):
-        self.curr_exc = None
-        self.curr_tb = None
-        self.curr_js_stack = None
+def format_stack(js_stack):
+    """
+    Returns a formatted Python-esque stack traceback of the given
+    JS stack.
+    """
 
-state = State()
-
-def throwhook(cx):
-    curr_exc = cx.get_pending_exception()
-    if state.curr_exc != curr_exc:
-        state.curr_exc = curr_exc
-        state.py_stack = traceback.extract_stack()
-        state.js_stack = cx.get_stack()
-
-cx.set_throw_hook(throwhook)
+    STACK_LINE  ="  File \"%(filename)s\", line %(lineno)d, in %(name)s"
 
-def watchdog():
-    while 1:
-        time.sleep(0.25)
-        cx.trigger_operation_callback()
-
-thread = threading.Thread(target=watchdog)
-thread.setDaemon(True)
-thread.start()
-
-filename = 'test.js'
-
-def make_stack(js_stack):
     lines = []
     while js_stack:
-        if js_stack['script']:
-            script = js_stack['script']
-            thing = dict(filename = script.filename,
+        script = js_stack['script']
+        function = js_stack['function']
+        if script:
+            frameinfo = dict(filename = script.filename,
                          lineno = js_stack['lineno'],
                          name = '<module>')
-        elif js_stack['function'] and not js_stack['function'].is_python:
-            func = js_stack['function']
-            thing = dict(filename = func.filename,
+        elif function and not function.is_python:
+            frameinfo = dict(filename = function.filename,
                          lineno = js_stack['lineno'],
-                         name = func.name)
+                         name = function.name)
         else:
-            thing = None
-        if thing:
-            lines.insert(0, "  File \"%(filename)s\", line %(lineno)d, in %(name)s" % thing)
+            frameinfo = None
+        if frameinfo:
+            lines.insert(0, STACK_LINE % frameinfo)
         js_stack = js_stack['caller']
     lines.insert(0, "Traceback (most recent call last):")
     return '\n'.join(lines)
 
-try:
-    cx.evaluate_script(globalobj, open(filename).read(), filename, 1)
-except pydermonkey.error, e:
-    print make_stack(state.js_stack)
-    print e.args[1]
-except InternalError, e:
-    print "An internal error occurred."
-    traceback.print_tb(e.exc_info[2])
-    print e.exc_info[1]
+class JSRuntime(object):
+    """
+    A JS runtime capable of loading and executing scripts.
+    """
+
+    def __init__(self, watchdog=watchdog):
+        rt = pydermonkey.Runtime()
+        cx = rt.new_context()
+        globalobj = cx.new_object()
+        cx.init_standard_classes(globalobj)
+
+        @safejsfunc(cx, globalobj)
+        def foo(cx, this, args):
+            return cx.call_function(this, args[0], ())
+
+        @safejsfunc(cx, globalobj, 'print')
+        def jsprint(cx, this, args):
+            if len(args) > 0:
+                print args[0]
+
+        cx.set_operation_callback(self._opcb)
+        cx.set_throw_hook(self._throwhook)
+        watchdog.add_context(cx)
+
+        self.rt = rt
+        self.cx = cx
+        self.globalobj = globalobj
+        self.curr_exc = None
+        self.py_stack = None
+        self.js_stack = None
+
+    def _opcb(self, cx):
+        # Don't do anything; if a keyboard interrupt was triggered,
+        # it'll get raised here automatically.
+        pass
+
+    def _throwhook(self, cx):
+        curr_exc = cx.get_pending_exception()
+        if self.curr_exc != curr_exc:
+            self.curr_exc = curr_exc
+            self.py_stack = traceback.extract_stack()
+            self.js_stack = cx.get_stack()
+
+    def run_script(self, filename):
+        contents = open(filename).read()
+        cx = self.cx
+        try:
+            cx.evaluate_script(self.globalobj, contents,
+                               filename, 1)
+        except pydermonkey.error, e:
+            print format_stack(self.js_stack)
+            print e.args[1]
+        except InternalError, e:
+            print "An internal error occurred."
+            traceback.print_tb(e.exc_info[2])
+            print e.exc_info[1]
+
+if __name__ == '__main__':
+    runtime = JSRuntime()
+    runtime.run_script('test.js')