view pydershell/pydershell.py @ 18:69e5523ebdc6

Separated out SafeJsObjectWrapper into itself and a new subclass, SafeJsFunctionWrapper, so that callable() works properly on instances.
author Atul Varma <varmaa@toolness.com>
date Mon, 07 Sep 2009 04:38:44 -0700
parents 1d62177c5c27
children 057260102960
line wrap: on
line source

#! /usr/bin/env python

import sys
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.
    """

    DEFAULT_INTERVAL = 0.25

    def __init__(self, interval=DEFAULT_INTERVAL):
        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()

class SafeJsObjectWrapper(object):
    """
    Securely wraps a JS object to behave like any normal Python object.
    """

    __slots__ = ['_jsobject', '_cx', '_this']

    def __init__(self, cx, jsobject, this):
        if not isinstance(jsobject, pydermonkey.Object):
            raise TypeError("Cannot wrap '%s' object" %
                            type(jsobject).__name__)
        object.__setattr__(self, '_jsobject', jsobject)
        object.__setattr__(self, '_cx', cx)
        object.__setattr__(self, '_this', this)

    @staticmethod
    def wrap(cx, jsvalue, this):
        if isinstance(jsvalue, pydermonkey.Function):
            return SafeJsFunctionWrapper(cx, jsvalue, this)
        elif isinstance(jsvalue, pydermonkey.Object):
            return SafeJsObjectWrapper(cx, jsvalue, this)
        else:
            # It's a primitive value.
            return jsvalue

    def _wrap_to_python(self, jsvalue):
        return self.wrap(self._cx, jsvalue, self._jsobject)

    def _wrap_to_js(self, value):
        # TODO: Add support for wrapping non-primitive python objects.
        return value

    def __eq__(self, other):
        if isinstance(other, SafeJsObjectWrapper):
            return self._jsobject == other._jsobject
        else:
            return False

    def __str__(self):
        return self.toString()

    def __unicode__(self):
        return self.toString()

    def __setitem__(self, item, value):
        self.__setattr__(item, value)

    def __setattr__(self, name, value):
        cx = self._cx
        jsobject = self._jsobject

        cx.define_property(jsobject, name,
                           self._wrap_to_js(value))

    def __getitem__(self, item):
        return self.__getattr__(item)

    def __getattr__(self, name):
        cx = self._cx
        jsobject = self._jsobject

        return self._wrap_to_python(cx.get_property(jsobject, name))

    def __contains__(self, item):
        cx = self._cx
        jsobject = self._jsobject

        return cx.has_property(jsobject, item)

    def __iter__(self):
        cx = self._cx
        jsobject = self._jsobject

        properties = cx.enumerate(jsobject)
        for property in properties:
            yield property

class SafeJsFunctionWrapper(SafeJsObjectWrapper):
    """
    Securely wraps a JS function to behave like any normal Python object.
    """

    def __init__(self, cx, jsfunction, this):
        if not isinstance(jsfunction, pydermonkey.Function):
            raise TypeError("Cannot wrap '%s' object" %
                            type(jsobject).__name__)
        SafeJsObjectWrapper.__init__(self, cx, jsfunction, this)

    def __call__(self, *args):
        cx = self._cx
        jsobject = self._jsobject
        this = self._this

        obj = cx.call_function(this, jsobject,
                               self._wrap_to_js(args))
        return self._wrap_to_python(obj)

def safejsfunc(cx, on_obj, name=None):
    """
    Exposes the decorated Python function on the given JS object.

    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__
        else:
            func_name = name
        def wrapper(func_cx, this, args):
            try:
                return func(func_cx, this, args)
            except pydermonkey.error:
                raise
            except Exception:
                raise InternalError()
        cx.define_property(
            on_obj,
            func_name, 
            cx.new_function(wrapper, func_name)
            )
        return func
    return make_wrapper

def format_stack(js_stack):
    """
    Returns a formatted Python-esque stack traceback of the given
    JS stack.
    """

    STACK_LINE  ="  File \"%(filename)s\", line %(lineno)d, in %(name)s"

    lines = []
    while js_stack:
        script = js_stack['script']
        function = js_stack['function']
        if script:
            frameinfo = dict(filename = script.filename,
                             lineno = js_stack['lineno'],
                             name = '<module>')
        elif function and not function.is_python:
            frameinfo = dict(filename = function.filename,
                             lineno = js_stack['lineno'],
                             name = function.name)
        else:
            frameinfo = None
        if frameinfo:
            lines.insert(0, STACK_LINE % frameinfo)
            try:
                filelines = open(frameinfo['filename']).readlines()
                line = filelines[frameinfo['lineno'] - 1].strip()
                lines.insert(1, "    %s" % line)
            except Exception:
                pass
        js_stack = js_stack['caller']
    lines.insert(0, "Traceback (most recent call last):")
    return '\n'.join(lines)

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 bar(cx, this, args):
            obj = SafeJsObjectWrapper.wrap(cx, args[0], this)
            print obj.bar()

        @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')
    del runtime

    import gc
    gc.collect()
    if pydermonkey.get_debug_info()['runtime_count']:
        print "WARNING: JS runtime was not destroyed."