├── chromedebug ├── boot │ ├── __init__.py │ └── sitecustomize.py ├── __init__.py ├── console.py ├── thread.py ├── profiler.py ├── inspector.py ├── server.py └── debugger.py ├── MANIFEST.in ├── .gitignore ├── setup.py └── README.md /chromedebug/boot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /chromedebug/boot/sitecustomize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | cur_path = os.path.dirname(__file__) 5 | parent_path = os.path.abspath(os.path.join(cur_path, '..', '..')) 6 | if not parent_path in sys.path: 7 | sys.path.append(parent_path) 8 | 9 | from chromedebug import debugger 10 | from chromedebug import thread 11 | 12 | #debugger.attach() 13 | thread.start() 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /chromedebug/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | __all__ = ['console', 'profiler'] 5 | 6 | 7 | def main(): 8 | cur_path = os.path.dirname(__file__) 9 | boot_path = os.path.join(cur_path, 'boot') 10 | if 'PYTHONPATH' in os.environ: 11 | os.environ['PYTHONPATH'] = '%s:%s' % ( 12 | boot_path, os.environ['PYTHONPATH']) 13 | else: 14 | os.environ['PYTHONPATH'] = boot_path 15 | os.execl(sys.executable, sys.executable, *sys.argv[1:]) 16 | -------------------------------------------------------------------------------- /chromedebug/console.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | import sys 3 | 4 | from . import debugger 5 | from . import thread 6 | 7 | __all__ = ['debug', 'error', 'log', 'warn'] 8 | 9 | 10 | def _get_trace(): 11 | frame = sys._getframe(2) 12 | trace = [] 13 | while frame: 14 | info = debugger.get_call_info(frame) 15 | trace.append({ 16 | 'functionName': info.function, 17 | 'url': info.module, 18 | 'lineNumber': info.lineno, 19 | 'columnNumber': 0}) 20 | frame = frame.f_back 21 | return trace 22 | 23 | 24 | def _log(level, *args): 25 | thread.console_log(level=level, typ='log', params=args, 26 | stack_trace=_get_trace()) 27 | 28 | debug = partial(_log, 'debug') 29 | error = partial(_log, 'error') 30 | log = partial(_log, 'log') 31 | warn = partial(_log, 'warning') 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | import os 5 | 6 | DESCRIPTION = open( 7 | os.path.join(os.path.dirname(__file__), 'README.md')).read().strip() 8 | 9 | setup( 10 | name='chromedebug', 11 | version='0.1a0', 12 | description='A Chrome debugger protocol server for Python', 13 | author='Patryk Zawadzki', 14 | author_email='patrys@room-303.com', 15 | url='https://github.com/patrys/chromedebug', 16 | packages=['chromedebug', 'chromedebug.boot'], 17 | keywords=['debugger', 'chrome'], 18 | classifiers=[ 19 | 'Development Status :: 3 - Alpha', 20 | 'Programming Language :: Python', 21 | 'Intended Audience :: Developers', 22 | 'Topic :: Software Development :: Debuggers', 23 | 'Operating System :: OS Independent'], 24 | install_requires=['ws4py'], 25 | entry_points={ 26 | 'console_scripts': ['chromedebug = chromedebug:main']}, 27 | long_description=DESCRIPTION) 28 | -------------------------------------------------------------------------------- /chromedebug/thread.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | from wsgiref.simple_server import make_server 4 | 5 | from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler 6 | from ws4py.server.wsgiutils import WebSocketWSGIApplication 7 | 8 | __all__ = ['start'] 9 | 10 | 11 | class ServerThread(threading.Thread): 12 | daemon = True 13 | name = 'ChromeDebug' 14 | server = None 15 | 16 | def run(self): 17 | from . import server 18 | self.server = make_server( 19 | '', 9222, server_class=WSGIServer, 20 | handler_class=WebSocketWSGIRequestHandler, 21 | app=WebSocketWSGIApplication(handler_cls=server.DebuggerWebSocket)) 22 | sys.stderr.write( 23 | 'Navigate to chrome://devtools/devtools.html?ws=0.0.0.0:9222\n') 24 | self.server.initialize_websockets_manager() 25 | self.server.serve_forever() 26 | 27 | thread = ServerThread() 28 | 29 | 30 | def start(): 31 | thread.start() 32 | 33 | 34 | def console_log(level, typ, params, stack_trace): 35 | if not thread.server: 36 | return 37 | for ws in thread.server.manager: 38 | ws.console_log(level=level, typ=typ, params=params, 39 | stack_trace=stack_trace) 40 | 41 | 42 | def timeline_log(message): 43 | if not thread.server: 44 | return 45 | for ws in thread.server.manager: 46 | ws.timeline_log(message) 47 | 48 | 49 | def debugger_paused(stack): 50 | if not thread.server: 51 | return 52 | for ws in thread.server.manager: 53 | ws.debugger_paused(stack) 54 | 55 | 56 | def debugger_resumed(): 57 | if not thread.server: 58 | return 59 | for ws in thread.server.manager: 60 | ws.debugger_resumed() 61 | 62 | 63 | def debugger_script_parsed(name): 64 | if not thread.server: 65 | return 66 | for ws in thread.server.manager: 67 | ws.debugger_script_parsed(name) 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | chromedebug 2 | =========== 3 | 4 | A Chrome remote debugging protocol server for Python. 5 | 6 | It's quite similar in concept to [Chrome Logger](http://craig.is/writing/chrome-logger/) but allows real-time communication and does not rely on any extensions. 7 | 8 | 9 | Setting up the server: 10 | ---------------------- 11 | 12 | Start by running your code using the `chromedebug` script. 13 | It will create a new thread and open a websocket for Chrome to connect to. 14 | 15 | ``` 16 | $ chromedebug myfile.py some args 17 | ``` 18 | 19 | Then navigate your browser (a recent release of Chrome is required) to the following url: 20 | 21 | ``` 22 | chrome://devtools/devtools.html?ws=0.0.0.0:9222 23 | ``` 24 | 25 | If Chrome claims that the URL is invalid, enable and disable the DevTools panel (F12) and then it will work. 26 | 27 | 28 | The console 29 | ----------- 30 | 31 | The `console` module offers an API similar to the one found in your browser: 32 | 33 | ```python 34 | from chromedebug import console 35 | 36 | console.log('Current time is', datetime.datetime.now()) 37 | console.warn('Oh my', None) 38 | console.error('EEEK!') 39 | ``` 40 | 41 | Avoid string interpolation and let the library serialize your objects instead. 42 | You can pass almost any object and then inspect its contents in the browser. 43 | 44 | 45 | The debugger 46 | ------------ 47 | 48 | This should Just Work™. 49 | 50 | ```python 51 | from chromedebug import debugger 52 | 53 | debugger.set_trace() 54 | ``` 55 | 56 | 57 | The profiler 58 | ------------ 59 | 60 | This should Just Work™. Yes, it does say *JavaScript* although it will happily profile your Python code. 61 | 62 | 63 | Alpha quality 64 | ------------- 65 | 66 | Please do not use this anywhere near production environments. This is a proof of concept. 67 | Current code hardly ever frees memory and needs lots of refactoring and probably a better API. 68 | 69 | **Warning:** Current implementation uses pure Python code which means it's *terribly slow*. 70 | -------------------------------------------------------------------------------- /chromedebug/profiler.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import time 3 | 4 | from . import debugger 5 | 6 | 7 | _uid = 0 8 | profilers = [] 9 | current_profiler = None 10 | 11 | 12 | class Profiler(object): 13 | 14 | def __init__(self, title): 15 | global _uid 16 | _uid += 1 17 | self.uid = _uid 18 | self.title = title 19 | self.children = {} 20 | self.samples = [] 21 | self.start_time = _get_timestamp() 22 | self.duration = None 23 | self.path = [] 24 | self._id = 1 25 | 26 | def _is_own_frame(self, frame): 27 | if not inspect: # terminating 28 | return True 29 | filename = inspect.getsourcefile(frame) 30 | # skip self 31 | filename_base = filename.rsplit('.', 1)[0] 32 | local_base = __file__.rsplit('.', 1)[0] 33 | if filename_base == local_base: 34 | return True 35 | return False 36 | 37 | def trace_call(self, call_info): 38 | if not self.path: 39 | if not call_info in self.children: 40 | self.children[call_info] = Trace(call_info, profiler=self) 41 | tracer = self.children[call_info] 42 | else: 43 | tracer = self.path[-1].add_child(call_info) 44 | self.samples.append(tracer.id) 45 | tracer.trace_call() 46 | self.path.append(tracer) 47 | 48 | def trace_return(self): 49 | if self.path: 50 | self.path[-1].trace_return() 51 | self.path.pop() 52 | 53 | def generate_id(self): 54 | self._id += 1 55 | return self._id 56 | 57 | def get_profile(self): 58 | if not self.duration: 59 | self.duration = _get_timestamp() - self.start_time 60 | return { 61 | 'head': { 62 | 'functionName': '(root)', 63 | 'url': '', 64 | 'lineNumber': 0, 65 | 'totalTime': self.duration, 66 | 'selfTime': 0, 67 | 'numberOfCalls': 0, 68 | 'visible': True, 69 | 'callUID': id(self), 70 | 'children': [c.encode() for c in self.children.values()], 71 | 'id': 1}, 72 | 'idleTime': self.duration - self.get_children_duration(), 73 | 'samples': self.samples} 74 | 75 | def get_header(self): 76 | return {'typeId': 'CPU', 'uid': self.uid, 'title': self.title} 77 | 78 | def get_children_duration(self): 79 | return sum(c.total_time for c in self.children.values()) 80 | 81 | 82 | class Trace(object): 83 | children = None 84 | in_call = False 85 | num_calls = 0 86 | 87 | def __init__(self, call_info, profiler): 88 | self.call_info = call_info 89 | self.profiler = profiler 90 | self.children = {} 91 | self.total_time = 0 92 | self.id = profiler.generate_id() 93 | 94 | def encode(self): 95 | function = self.call_info.function 96 | if self.in_call: 97 | function += ' (did not return)' 98 | return { 99 | 'functionName': function, 100 | 'url': self.call_info.module, 101 | 'lineNumber': self.call_info.lineno, 102 | 'totalTime': self.total_time, 103 | 'selfTime': self.total_time - self.get_children_duration(), 104 | 'numberOfCalls': self.num_calls, 105 | 'visible': True, 106 | 'callUID': id(self), 107 | 'children': [c.encode() for c in self.children.values()], 108 | 'id': self.id} 109 | 110 | def get_samples(self): 111 | return sum((c.get_samples() for c in self.children.values()), [id(self)]) 112 | 113 | def get_children_duration(self): 114 | return sum(c.total_time for c in self.children.values()) 115 | 116 | def add_child(self, call_info): 117 | if not call_info in self.children: 118 | self.children[call_info] = Trace(call_info, profiler=self.profiler) 119 | return self.children[call_info] 120 | 121 | def trace_call(self): 122 | self.in_call = True 123 | self.num_calls += 1 124 | self.start_time = _get_timestamp() 125 | 126 | def trace_return(self): 127 | self.in_call = False 128 | self.total_time += _get_timestamp() - self.start_time 129 | 130 | 131 | def start_profiling(name=None): 132 | next_num = _uid + 1 133 | name = name or 'Python %d' % (next_num,) 134 | global current_profiler 135 | current_profiler = Profiler(name) 136 | profilers.append(current_profiler) 137 | debugger.attach_profiler(current_profiler) 138 | 139 | 140 | def stop_profiling(): 141 | global current_profiler 142 | debugger.detach_profiler(current_profiler) 143 | header = current_profiler.get_header() 144 | current_profiler = None 145 | return header 146 | 147 | 148 | def get_profile(uid): 149 | ps = [p for p in profilers if p.uid == uid] 150 | if ps: 151 | return ps[0].get_profile() 152 | 153 | 154 | def get_profile_headers(): 155 | return [p.get_header() for p in profilers if p != current_profiler] 156 | 157 | 158 | def _get_timestamp(): 159 | if not time: # terminating 160 | return 0 161 | return time.time() * 1000.0 162 | -------------------------------------------------------------------------------- /chromedebug/inspector.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, namedtuple 2 | import types 3 | import weakref 4 | 5 | 6 | properties = {} 7 | groups = defaultdict(list) 8 | 9 | 10 | Property = namedtuple('Property', 'name value bound enumerable descriptor') 11 | 12 | 13 | def inspect(obj): 14 | if isinstance(obj, (frozenset, list, set, tuple)): 15 | for i, v in enumerate(obj): 16 | yield Property(unicode(i), v, True, False, False) 17 | elif isinstance(obj, dict): 18 | for k, v in obj.iteritems(): 19 | yield Property(k, v, True, False, False) 20 | else: 21 | if hasattr(obj, '__slots__'): 22 | for k in obj.__slots__: 23 | if not k.startswith('_'): 24 | yield Property(k, getattr(obj, k), True, True, False) 25 | if hasattr(obj, '__dict__'): 26 | for k, v in obj.__dict__.iteritems(): 27 | if not k.startswith('_'): 28 | yield Property(k, v, True, True, False) 29 | if isinstance(obj, object): 30 | for k, v in type(obj).__dict__.iteritems(): 31 | if k.startswith('_'): 32 | continue 33 | if hasattr(obj, '__dict__') and k in obj.__dict__: 34 | continue 35 | if hasattr(obj, '__slots__') and k in obj.__slots__: 36 | continue 37 | if isinstance(v, property): 38 | yield Property(k, v, False, True, True) 39 | else: 40 | yield Property(k, v, False, True, False) 41 | 42 | 43 | def extract_properties(obj, accessors=False): 44 | for prop in inspect(obj): 45 | if bool(accessors) != bool(prop.descriptor): 46 | continue 47 | data = {'name': prop.name, 48 | 'configurable': False, 49 | 'enumerable': prop.enumerable, 50 | 'wasThrown': False, 51 | 'isOwn': prop.bound} 52 | if prop.descriptor: 53 | if prop.value.getter: 54 | data['get'] = encode(prop.value.fget) 55 | if prop.value.setter: 56 | data['set'] = encode(prop.value.fset) 57 | data['writable'] = prop.value.fset is not None 58 | else: 59 | data['value'] = encode(prop.value) 60 | yield data 61 | 62 | 63 | def get_object(object_id): 64 | try: 65 | object_id = int(object_id) 66 | obj = properties[object_id] 67 | except Exception: 68 | return [] 69 | if isinstance(obj, weakref.ref): 70 | obj = obj() 71 | return obj 72 | 73 | 74 | def get_function_details(object_id): 75 | try: 76 | object_id = int(object_id) 77 | obj = properties[object_id] 78 | except Exception: 79 | return None 80 | if isinstance(obj, weakref.ref): 81 | obj = obj() 82 | code = obj.func_code 83 | return { 84 | 'location': {'scriptId': obj.__module__, 85 | 'lineNumber': code.co_firstlineno - 1}, 86 | 'name': code.co_name, 87 | 'displayName': obj.__name__} 88 | 89 | 90 | def add_obj_to_group(obj, group): 91 | if not obj in groups[group]: 92 | groups[group].append(obj) 93 | save_properties(obj) 94 | 95 | 96 | def save_properties(obj): 97 | object_id = id(obj) 98 | if object_id in properties: 99 | return str(object_id) 100 | try: 101 | data = weakref.ref(obj) 102 | except: 103 | data = obj 104 | properties[object_id] = data 105 | return str(object_id) 106 | 107 | 108 | def get_type(obj): 109 | if isinstance(obj, bool): 110 | return 'boolean' 111 | elif isinstance(obj, (float, int)): 112 | return 'number' 113 | elif isinstance(obj, (str, unicode)): 114 | return 'string' 115 | elif isinstance(obj, (types.FunctionType, types.MethodType, 116 | types.UnboundMethodType, classmethod, staticmethod)): 117 | return 'function' 118 | else: 119 | return 'object' 120 | 121 | 122 | def get_subtype(obj): 123 | if isinstance(obj, (dict, frozenset, list, set, tuple)): 124 | return 'array' 125 | elif isinstance(obj, types.NoneType): 126 | return 'null' 127 | 128 | 129 | def encode_property(prop): 130 | typ = get_type(prop.value) 131 | subtype = get_subtype(prop.value) 132 | data = {'name': prop.name, 'type': typ} 133 | if subtype: 134 | data['subtype'] = subtype 135 | if typ in ['boolean', 'number', 'string']: 136 | data['value'] = unicode(prop.value) 137 | else: 138 | data['valuePreview'] = repr(prop.value) 139 | return data 140 | 141 | 142 | def preview_array(obj): 143 | preview = {'lossless': True} 144 | props = list(inspect(obj))[:11] 145 | if len(props) > 10: 146 | preview['overflow'] = True 147 | props = props[:10] 148 | else: 149 | preview['overflow'] = False 150 | preview['properties'] = [encode_property(prop) for prop in props] 151 | return preview 152 | 153 | 154 | def encode_array(obj, preview=False, by_value=False): 155 | data = {} 156 | data['objectId'] = save_properties(obj) 157 | data['description'] = '%s() [%d]' % (type(obj).__name__, len(obj)) 158 | if preview: 159 | data['preview'] = preview_array(obj) 160 | return data 161 | 162 | 163 | def encode_function(obj, preview=False, by_value=False): 164 | data = {} 165 | data['objectId'] = save_properties(obj) 166 | prefix = '' 167 | if isinstance(obj, classmethod): 168 | prefix = '@classmethod ' 169 | obj = obj.__func__ 170 | if isinstance(obj, staticmethod): 171 | prefix = '@staticmethod ' 172 | obj = obj.__func__ 173 | if hasattr(obj, 'im_func'): 174 | obj = obj.im_func 175 | data['description'] = u'%(prefix)sdef %(name)s(%(params)s):' % { 176 | 'prefix': prefix, 177 | 'name': obj.__name__, 178 | 'params': ', '.join(obj.func_code.co_varnames) 179 | } 180 | return data 181 | 182 | 183 | def encode_none(obj, preview=False, by_value=False): 184 | return {'value': None, 'description': 'None'} 185 | 186 | 187 | def encode_object(obj, preview=False, by_value=False): 188 | data = {} 189 | data['objectId'] = save_properties(obj) 190 | data['className'] = type(obj).__name__ 191 | data['description'] = repr(obj) 192 | return data 193 | 194 | 195 | def encode_value(obj, preview=False, by_value=False): 196 | return {'value': obj} 197 | 198 | 199 | ENCODERS = [ 200 | ('boolean', None, encode_value), 201 | ('function', None, encode_function), 202 | ('number', None, encode_value), 203 | ('object', 'array', encode_array), 204 | ('object', 'null', encode_none), 205 | ('object', None, encode_object), 206 | ('string', None, encode_value)] 207 | 208 | 209 | def encode(obj, preview=False, by_value=False): 210 | data = {} 211 | typ = get_type(obj) 212 | subtype = get_subtype(obj) 213 | data['type'] = typ 214 | if subtype: 215 | data['subtype'] = subtype 216 | if by_value and isinstance(obj, dict): 217 | data['value'] = obj 218 | for data_type, data_subtype, encoder in ENCODERS: 219 | if typ == data_type and subtype == data_subtype: 220 | specialized = encoder(obj, preview=preview, by_value=by_value) 221 | data.update(specialized) 222 | return data 223 | 224 | 225 | def release_group(group): 226 | if group in groups: 227 | for object_id in groups[group]: 228 | if object_id in properties: 229 | if not isinstance(properties[object_id], weakref.ref): 230 | del properties[object_id] 231 | del groups[group] 232 | -------------------------------------------------------------------------------- /chromedebug/server.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from ws4py.websocket import WebSocket 5 | 6 | from . import debugger 7 | from . import inspector 8 | from . import profiler 9 | 10 | 11 | class DebuggerWebSocket(WebSocket): 12 | console_cache = None 13 | console_enabled = False 14 | debugger_enabled = False 15 | profiling_enabled = False 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(DebuggerWebSocket, self).__init__(*args, **kwargs) 19 | self.console_messages = [] 20 | self.console_cache = [] 21 | self._call_stack = [] 22 | 23 | def handle_method(self, method, params): 24 | resp = {} 25 | if not debugger or not inspector or not profiler: # terminating 26 | return 27 | if method == 'Console.disable': 28 | self.console_enabled = False 29 | elif method == 'Console.enable': 30 | self.console_enabled = True 31 | self.console_flush() 32 | elif method == 'Debugger.canSetScriptSource': 33 | resp['result'] = False 34 | elif method == 'Debugger.continueToLocation': 35 | location = params.get('location', {}) 36 | debugger.continue_to( 37 | location.get('scriptId'), 38 | location.get('lineNumber')) 39 | elif method == 'Debugger.disable': 40 | self.debugger_enabled = False 41 | elif method == 'Debugger.enable': 42 | self.debugger_enabled = True 43 | for name, module in sys.modules.iteritems(): 44 | if module: 45 | self.debugger_script_parsed(name) 46 | info = debugger.get_state() 47 | if info: 48 | self.debugger_paused(info) 49 | elif method == 'Debugger.evaluateOnCallFrame': 50 | expression = params.get('expression', '') 51 | preview = params.get('generatePreview', False) 52 | object_group = params.get('objectGroup', None) 53 | result = debugger.evaluate_on_frame( 54 | params.get('callFrameId'), expression, 55 | group=object_group, preview=preview) 56 | resp['result'] = result 57 | elif method == 'Debugger.getFunctionDetails': 58 | object_id = params.get('functionId') 59 | props = inspector.get_function_details(object_id) 60 | resp['result'] = {'details': props} 61 | elif method == 'Debugger.getScriptSource': 62 | content = debugger.get_script_source(params.get('scriptId')) 63 | resp['result'] = {'scriptSource': content} 64 | elif method == 'Debugger.pause': 65 | debugger.pause() 66 | elif method == 'Debugger.removeBreakpoint': 67 | debugger.remove_breakpoint(params.get('breakpointId')) 68 | elif method == 'Debugger.setBreakpointByUrl': 69 | breakpoint = debugger.add_breakpoint(params.get('url'), 70 | params.get('lineNumber')) 71 | resp['result'] = breakpoint 72 | elif method == 'Debugger.setBreakpointsActive': 73 | debugger.set_active(params.get('active')) 74 | elif method == 'Debugger.stepInto': 75 | debugger.step_into() 76 | elif method == 'Debugger.stepOver': 77 | debugger.step_over() 78 | elif method == 'Debugger.stepOut': 79 | debugger.step_out() 80 | elif method == 'Debugger.resume': 81 | debugger.resume() 82 | elif method == 'Debugger.setOverlayMessage': 83 | msg = params.get('message') 84 | if msg: 85 | sys.stderr.write('<< %s >>\n' % (msg,)) 86 | elif method == 'Page.enable': 87 | resp['error'] = {} 88 | elif method == 'Profiler.start': 89 | profiler.start_profiling() 90 | self.send_event('Profiler.setRecordingProfile', isProfiling=True) 91 | elif method == 'Profiler.stop': 92 | header = profiler.stop_profiling() 93 | self.send_event('Profiler.addProfileHeader', header=header) 94 | self.send_event('Profiler.setRecordingProfile', isProfiling=False) 95 | elif method == 'Profiler.getProfileHeaders': 96 | headers = profiler.get_profile_headers() 97 | resp['result'] = {'headers': headers} 98 | elif method == 'Profiler.getCPUProfile': 99 | profile = profiler.get_profile(params.get('uid')) 100 | resp['result'] = {'profile': profile} 101 | elif method == 'Runtime.callFunctionOn': 102 | # hacks! 103 | object_id = params.get('objectId') 104 | body = params.get('functionDeclaration', '') 105 | if body.startswith('function getCompletions(primitiveType)'): 106 | obj = inspector.get_object(object_id) 107 | props = inspector.extract_properties(obj) 108 | props = dict((p['name'], True) for p in props) 109 | resp['result'] = { 110 | 'result': inspector.encode(props, by_value=True)} 111 | elif body.startswith('function remoteFunction(arrayStr)'): 112 | props = params.get('arguments') 113 | if props: 114 | props = props[0].get('value') 115 | if props: 116 | props = json.loads(props) 117 | obj = inspector.get_object(object_id) 118 | for prop in props: 119 | try: 120 | obj = getattr(obj, prop) 121 | except Exception: 122 | break 123 | resp['result'] = { 124 | 'result': inspector.encode(obj, by_value=True)} 125 | else: 126 | resp['error'] = { 127 | 'message': '%s not supported' % (method,), 128 | 'data': {}} 129 | elif method == 'Runtime.getProperties': 130 | object_id = params.get('objectId') 131 | accessor = params.get('accessorPropertiesOnly', False) 132 | obj = inspector.get_object(object_id) 133 | props = inspector.extract_properties(obj, accessors=accessor) 134 | resp['result'] = {'result': list(props)} 135 | elif method == 'Runtime.releaseObjectGroup': 136 | object_group = params.get('objectGroup', None) 137 | inspector.release_group(object_group) 138 | else: 139 | resp['error'] = { 140 | 'message': '%s not supported' % (method,), 141 | 'data': {}} 142 | return resp 143 | 144 | def debugger_paused(self, stack): 145 | if not debugger: # terminating 146 | return 147 | if not self.debugger_enabled: 148 | debugger.resume() 149 | self.send_event('Debugger.paused', **stack) 150 | 151 | def debugger_resumed(self): 152 | self.send_event('Debugger.resumed') 153 | 154 | def debugger_script_parsed(self, name): 155 | if not self.debugger_enabled: 156 | return 157 | self.send_event('Debugger.scriptParsed', scriptId=name, 158 | url=name, startLine=0, startColumn=0, 159 | endLine=0, endColumn=0) 160 | 161 | def console_log(self, level, typ, params, stack_trace): 162 | # hold a reference 163 | self.console_cache.append(params) 164 | params = map(inspector.encode, params) 165 | message = { 166 | 'level': level, 167 | 'type': typ, 168 | 'parameters': params, 169 | 'stackTrace': stack_trace} 170 | self.console_messages.append(message) 171 | self.console_flush() 172 | 173 | def console_flush(self): 174 | if not self.console_enabled: 175 | return 176 | msgs = self.console_messages 177 | self.console_messages = [] 178 | for msg in msgs: 179 | self.send_event('Console.messageAdded', message=msg) 180 | 181 | def timeline_log(self, record): 182 | if not self.tracing_enabled: 183 | return 184 | self.send_event('Timeline.eventRecorded', record=record) 185 | 186 | def send_event(self, method, **kwargs): 187 | self.send(json.dumps({'method': method, 'params': kwargs})) 188 | 189 | def received_message(self, message): 190 | try: 191 | msg = json.loads(message.data) 192 | except Exception: 193 | return 194 | response = self.handle_method( 195 | msg['method'], msg.get('params', {})) 196 | response.update(id=msg['id']) 197 | self.send(json.dumps(response)) 198 | -------------------------------------------------------------------------------- /chromedebug/debugger.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | from collections import defaultdict, namedtuple 3 | import fnmatch 4 | from functools import wraps 5 | import inspect 6 | import sys 7 | import threading 8 | 9 | from . import inspector 10 | from . import thread 11 | 12 | seen = set() 13 | 14 | CallInfo = namedtuple('CallInfo', ['function', 'module', 'lineno']) 15 | debug_lock = threading.Lock() 16 | 17 | 18 | def get_call_info(frame): 19 | if not inspect: # terminating 20 | return 21 | info = inspect.getframeinfo(frame) 22 | values = inspect.getargvalues(frame) 23 | function = info.function 24 | if values.args and values.args[0] == 'self': 25 | klass = type(values.locals[values.args[0]]) 26 | function = '%s.%s' % (klass.__name__, function) 27 | elif function == '__new__' and values.args: 28 | klass = values.locals[values.args[0]] 29 | if isinstance(klass, type): 30 | function = '%s.%s' % (klass.__name__, function) 31 | module = inspect.getmodule(frame) 32 | if module: 33 | module = module.__name__ 34 | else: 35 | module = '(unknown)' 36 | return CallInfo(function, module, info.lineno) 37 | 38 | 39 | class Debugger(object): 40 | 41 | breakpoints_active = True 42 | profilers = None 43 | current_frame = None 44 | step_mode = None 45 | step_level = 0 46 | stop_module = None 47 | stop_lineno = None 48 | 49 | def __init__(self, skip=None): 50 | self.profilers = set() 51 | self.resume = threading.Event() 52 | self.skip = set(skip) if skip else None 53 | self.breaks = defaultdict(set) 54 | self.fncache = {} 55 | 56 | def trace_dispatch(self, frame, event, arg): 57 | if self.skip and self.is_skipped(frame): 58 | return 59 | if event == 'line': 60 | return self.dispatch_line(frame) 61 | if event == 'call': 62 | return self.dispatch_call(frame, arg) 63 | if event == 'return': 64 | return self.dispatch_return(frame, arg) 65 | 66 | def dispatch_line(self, frame): 67 | if not self.step_mode: 68 | if not self.breakpoints_active: 69 | return 70 | call_info = get_call_info(frame) 71 | if not self.step_mode: 72 | if not call_info.module in self.breaks: 73 | return 74 | if self.stop_here(call_info) or self.break_here(call_info): 75 | self.pause(frame) 76 | return self.trace_dispatch 77 | 78 | def dispatch_call(self, frame, arg): 79 | call_info = get_call_info(frame) 80 | if not self.profilers: 81 | if not self.step_mode: 82 | if not self.breakpoints_active: 83 | return 84 | call_info = get_call_info(frame) 85 | if call_info.module in self.breaks: 86 | return 87 | if self.step_mode in ['over', 'out']: 88 | self.step_level += 1 89 | for profiler in self.profilers: 90 | profiler.trace_call(call_info) 91 | return self.trace_dispatch 92 | 93 | def dispatch_return(self, frame, arg): 94 | if self.step_mode in ['over', 'out']: 95 | self.step_level -= 1 96 | if self.step_mode == 'out' and self.step_level < 0: 97 | self.pause(frame) 98 | for profiler in self.profilers: 99 | profiler.trace_return() 100 | 101 | def is_skipped(self, frame): 102 | if not fnmatch: 103 | return True 104 | while frame: 105 | module = frame.f_globals.get('__name__') 106 | if not module: 107 | return True 108 | for pattern in self.skip: 109 | if fnmatch.fnmatch(module, pattern): 110 | return True 111 | frame = frame.f_back 112 | return False 113 | 114 | def stop_here(self, call_info): 115 | if self.step_mode == 'into': 116 | return True 117 | if self.step_mode == 'over' and self.step_level <= 0: 118 | return True 119 | if self.step_mode == 'out' and self.step_level < 0: 120 | return True 121 | if call_info.module == self.stop_module: 122 | if call_info.lineno >= self.stop_lineno: 123 | return True 124 | return False 125 | 126 | def break_here(self, call_info): 127 | if not call_info.module in self.breaks: 128 | return False 129 | if not call_info.lineno in self.breaks[call_info.module]: 130 | return False 131 | return True 132 | 133 | def break_anywhere(self, frame): 134 | if not inspect: 135 | return False 136 | mod = inspect.getmodule(frame) 137 | if not mod: 138 | return False 139 | module = mod.__name__ 140 | return module in self.breaks 141 | 142 | def _extract_frames(self, frame): 143 | info = get_call_info(frame) 144 | location = { 145 | 'scriptId': info.module, 146 | 'lineNumber': info.lineno - 1} 147 | scope_chain = [ 148 | {'type': 'local', 149 | 'object': inspector.encode(frame.f_locals, preview=False)}, 150 | {'type': 'global', 151 | 'object': inspector.encode(frame.f_globals, preview=False)}] 152 | frame_id = str(id(frame)) 153 | frames = [{ 154 | 'callFrameId': frame_id, 155 | 'functionName': info.function, 156 | 'location': location, 157 | 'scopeChain': scope_chain}] 158 | if frame.f_back and frame.f_back is not self.source_frame: 159 | frames += self._extract_frames(frame.f_back) 160 | return frames 161 | 162 | def evaluate_on_frame(self, frame_id, expression): 163 | frame = self.current_frame 164 | while frame and str(id(frame)) != frame_id: 165 | frame = frame.f_back 166 | if not frame: 167 | return None 168 | try: 169 | return eval(expression, frame.f_globals, frame.f_locals) 170 | except: 171 | exec(expression, frame.f_globals, frame.f_locals) 172 | 173 | def get_pause_info(self): 174 | if not self.current_frame: 175 | return 176 | frames = self._extract_frames(self.current_frame) 177 | return {'callFrames': frames, 'reason': 'other'} 178 | 179 | def pause(self, frame): 180 | if not thread or not threading: # terminating 181 | return 182 | if threading.current_thread().name == 'ChromeDebug': 183 | return 184 | if not self.breakpoints_active: 185 | return 186 | with debug_lock: 187 | if self.current_frame: 188 | return 189 | self.current_frame = frame 190 | self.resume.clear() 191 | info = self.get_pause_info() 192 | thread.debugger_paused(info) 193 | self.resume.wait() 194 | with debug_lock: 195 | self.current_frame = None 196 | 197 | def set_continue(self): 198 | self.step_mode = None 199 | self.stop_module = None 200 | self.stop_lineno = None 201 | thread.debugger_resumed() 202 | self.resume.set() 203 | 204 | def continue_to(self, module, lineno): 205 | self.step_mode = None 206 | self.stop_module = module 207 | self.stop_lineno = lineno 208 | thread.debugger_resumed() 209 | self.resume.set() 210 | 211 | def set_step(self, mode): 212 | self.step_mode = mode 213 | self.step_level = 0 214 | self.stop_module = None 215 | self.stop_lineno = None 216 | thread.debugger_resumed() 217 | self.resume.set() 218 | 219 | def set_break(self, module, lineno): 220 | self.breaks[module].add(lineno) 221 | 222 | def set_breakpoints_active(self, active): 223 | self.breakpoints_active = active 224 | 225 | def attach_profiler(self, profiler): 226 | self.profilers.add(profiler) 227 | 228 | def detach_profiler(self, profiler): 229 | self.profilers.remove(profiler) 230 | 231 | def clear_break(self, module, lineno): 232 | if module in self.breaks: 233 | if lineno in self.breaks[module]: 234 | self.breaks[module].remove(lineno) 235 | if not self.breaks[module]: 236 | del self.breaks[module] 237 | 238 | def attach(self): 239 | try: 240 | self.source_frame = sys._getframe(3) 241 | except ValueError: 242 | self.source_frame = None 243 | sys.settrace(self.trace_dispatch) 244 | 245 | def detach(self): 246 | sys.settrace(None) 247 | self.source_frame = None 248 | 249 | def set_trace(self): 250 | frame = sys._getframe().f_back 251 | while frame: 252 | frame.f_trace = debugger.trace_dispatch 253 | frame = frame.f_back 254 | if not sys.gettrace(): 255 | self.attach() 256 | self.set_step('into') 257 | 258 | 259 | def get_script_source(scriptId): 260 | module = sys.modules.get(scriptId) 261 | if not module: 262 | return '"Module not found"' 263 | try: 264 | return inspect.getsource(module) 265 | except IOError: 266 | return '"Source not available"' 267 | except TypeError: 268 | return '"Built-in module"' 269 | 270 | debugger = Debugger(skip=['chromedebug', 'chromedebug.*', 'ws4py.*']) 271 | 272 | 273 | def attach(): 274 | debugger.attach() 275 | 276 | 277 | def detach(): 278 | debugger.detach() 279 | atexit.register(detach) 280 | 281 | 282 | def trace(func): 283 | @wraps(func) 284 | def inner(*args, **kwargs): 285 | attach() 286 | try: 287 | return func(*args, **kwargs) 288 | finally: 289 | detach() 290 | 291 | return inner 292 | 293 | 294 | def exempt(func): 295 | @wraps(func) 296 | def inner(*args, **kwargs): 297 | old_trace = sys.gettrace() 298 | sys.settrace(None) 299 | try: 300 | return func(*args, **kwargs) 301 | finally: 302 | sys.settrace(old_trace) 303 | 304 | return inner 305 | 306 | 307 | def add_breakpoint(url, lineno): 308 | debugger.set_break(url, lineno + 1) 309 | return { 310 | 'breakpointId': '%s:%s' % (url, lineno), 311 | 'locations': [{'scriptId': url, 'lineNumber': lineno}]} 312 | 313 | 314 | def evaluate_on_frame(frame_id, expression, group=None, preview=False): 315 | try: 316 | obj = debugger.evaluate_on_frame(frame_id, expression) 317 | if group: 318 | inspector.add_obj_to_group(obj, group) 319 | return {'result': inspector.encode(obj, preview=preview)} 320 | except Exception, e: 321 | return { 322 | 'result': inspector.encode(e), 323 | 'wasThrown': True} 324 | 325 | 326 | def get_state(): 327 | return debugger.get_pause_info() 328 | 329 | 330 | def pause(): 331 | debugger.set_step('into') 332 | 333 | 334 | def remove_breakpoint(break_id): 335 | module, lineno = break_id.split(':', 1) 336 | debugger.clear_break(module, int(lineno) + 1) 337 | 338 | 339 | def resume(): 340 | debugger.set_continue() 341 | 342 | 343 | def continue_to(url, lineno): 344 | debugger.continue_to(url, lineno) 345 | 346 | 347 | def set_breakpoints_active(active): 348 | debugger.set_breakpoints_active(active) 349 | 350 | 351 | def attach_profiler(profiler): 352 | debugger.attach_profiler(profiler) 353 | 354 | 355 | def detach_profiler(profiler): 356 | debugger.detach_profiler(profiler) 357 | 358 | 359 | def step_into(): 360 | debugger.set_step('into') 361 | 362 | 363 | def step_over(): 364 | debugger.set_step('over') 365 | 366 | 367 | def step_out(): 368 | debugger.set_step('out') 369 | 370 | 371 | def set_trace(): 372 | debugger.set_trace() 373 | --------------------------------------------------------------------------------