view pydershell/pydershell.py @ 16:a78570a423ea

Show code excerpts in js stack tracebacks
author Atul Varma <varmaa@toolness.com>
date Mon, 07 Sep 2009 00:33:56 -0700
parents 351525e95a45
children 1d62177c5c27
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()

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