blob: c4654a9553f443962e5664cf1b7dbb0399a4e7e6 [file] [log] [blame]
'''
Copyright (c) 2018 Martin Kloesch
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
which accompanies this distribution, and is available at
http://www.eclipse.org/legal/epl-v10.html
Contributors:
* Martin Kloesch - initial API and implementation
'''
# Python std library imports
import ast
import bdb
import functools
import re
import sys
import threading
# : Regular expression if we are dealing with internal module.
INTERNAL_CHECKER = re.compile(r'^<.+>$')
class PyFrame:
'''
Python implementation of IPyFrame used for exchanging frame data
with eclipse.
Simply wraps standard python frame to more easily usable format.
Python frame documentation is available here:
https://docs.python.org/3/reference/datamodel.html#types
'''
def __init__(self, frame):
'''
Constructor only stores frame to member.
:param frame: Python frame to be converted.
'''
self._frame = frame
def _valid_frame(self):
'''
Utility method checking if the current frame is valid
A frame is valid if all information is present and
it's file is not ignored.
:returns: ``True`` if frame is valid.
'''
return self._frame and not ignore_frame(self._frame)
def getFilename(self):
'''
Returns the filename of the frame.
If no frame was set, a dummy value will be returned.
:returns: Filename of frame or dummy value.
'''
if self._valid_frame():
return self._frame.f_code.co_filename
else:
return '__no_frame__'
def getLineNumber(self):
'''
Returns the linenumber of the frame.
If no frame was set, -1 is returned.
:returns: Line number of frame or -1.
'''
if self._valid_frame():
return self._frame.f_lineno
else:
return -1
def getParent(self):
'''
Returns the parent of the frame.
If no frame was set or the frame does not have a parent
`None` is returned.
:returns: Parent frame in stack or `None`
'''
if self._valid_frame():
return PyFrame(self._frame.f_back)
else:
return None
def getVariable(self, name):
'''
Get a variable from the current frame.
:param: name: variable name to look up
:return: variable or <code>null</code>
'''
if self._valid_frame():
if name in self._frame.f_locals:
return convert_value(self._frame.f_locals[name])
if name in self._frame.f_globals:
return convert_value(self._frame.f_globals[name])
return None
def getVariables(self):
'''
Get variables visible from current frame.
:return: variableName -> variableContent
'''
variables = {}
if self._valid_frame():
# TODO: Differentiate between global and local variables
variables.update(self._frame.f_globals)
variables.update(self._frame.f_locals)
return convert_value(variables)
def setVariable(self, name, value):
'''
Sets new value for a variable only if it exists.
:param name: Name of variable to be updated.
:param value: New value for variable.
'''
if self._valid_frame():
if name in self._frame.f_locals:
self._frame.f_locals[name] = value
elif name in self._frame.f_globals:
self._frame.f_globals[name] = value
def toString(self):
'''
Required override to avoid problems in py4j bridge.
'''
return '{}: #{:d}'.format(self.getFilename(), self.getLineNumber())
class Java:
implements = ['org.eclipse.ease.lang.python.debugger.IPyFrame']
def ignore_frame(frame, first=True):
'''
Utility to check if a frame should be ignored.
Current reasons to ignore a frame:
* It is the top entry of the stack and it is a standard module.
e.g. <string>
* It is part of the py4j library or has py4j in its call chain.
Recursively checks trace until at bottom of stack.
:param frame: Frame to check if it should be ignored.
:param first: Flag to signalize if it is the first call.
:returns: `True` if the frame should be ignored.
'''
# End of frame
if not frame:
return False
# ignore frames that originate from the self.run() method
if first:
if frame.f_code.co_filename == '(none)':
return True
# Check if we are in the standard modules
if INTERNAL_CHECKER.match(frame.f_code.co_filename):
# Only ignore standard module if its the top of the stack
return first
# TODO: Think of better way to identify py4j library
if 'py4j' in frame.f_code.co_filename.lower():
return True
return ignore_frame(frame.f_back, False)
class CodeTracer(bdb.Bdb):
'''
Eclipse Debugger class.
'''
def __init__(self, *args, **kwargs):
'''
Constructor initializes instance variables.
'''
bdb.Bdb.__init__(self, *args, **kwargs)
# Debugger for communication with Java world
self._debugger = None
# Caches for currently executed code parts
self._current_frame = None
self._current_file = None
# Async continuation handling
self._continue_event = threading.Event()
self._continue_func = lambda: None
# TODO: Think about a better way to handle step return
self._return_hack = False
def setDebugger(self, debugger):
'''
Setter method for self._debugger.
:param org.eclipse.ease.lang.python.debugger.PythonDebugger debugger:
PythonDebugger object to handling communication with Eclipse.
'''
self._debugger = debugger
def trace_dispatch(self, frame, event, arg):
'''
Called for every executed line of source code.
Checks if line is of interest and dispatches the call for
further investigation.
:param frame: Current stack frame.
:param event: Type of dispatch event that occurred.
:param arg: Optional argument for dispatching.
'''
# Pre-filter to avoid issues with library internals
if not ignore_frame(frame):
bdb.Bdb.trace_dispatch(self, frame, event, arg)
return self.trace_dispatch
def user_line(self, frame):
'''
Called when debugger thinks line is of interest.
:param frame: Current stack frame with line.
'''
self._continue_func = self.set_continue
self.dispatch(frame, 'line')
def user_call(self, frame, argument_list):
'''
Called when debugger thinks call is of interest.
:param frame: Current stack frame with call information.
:param argument_list: ignored
'''
self.dispatch(frame, 'call')
def user_return(self, frame, return_value):
'''
Called when debugger thinks returning from function is of interest.
:param frame: Current stack frame with return information.
:param return_value: ignored
'''
self._continue_func = self.set_step
self.dispatch(frame, 'return')
def user_exception(self, frame, exc_info):
'''
Called when debugger thinks thrown exception is of interest.
:param frame: Current stack frame with exception information.
:param exc_info: ignored
'''
# Cache exception
if self._debugger:
self._debugger.setExceptionStackTrace(PyFrame(frame))
self._continue_func = self.set_continue
self.dispatch(frame, 'exception')
def dispatch(self, frame, dispatch_type):
'''
Dispatches the given frame to the interested debugger.
:param frame: Current stack frame to be dispatched.
:param dispatch_type: Type of dispatch event (call, line, ...)
'''
if not self._debugger:
return
# Set up caches for asynchronous continuation
self._current_frame = frame
self._continue_event.set()
# Check if we need to fake the current frame for Eclipse
if self._return_hack:
frame = frame.f_back
self._return_hack = False
# Dispatch call to Java
self._debugger.traceDispatch(PyFrame(frame), dispatch_type)
# Make asynchronous call synchronous again
self._continue_event.wait()
self._continue_func()
def suspend(self):
'''
Suspend the execution to wait for asynchronous callbacks
from Java.
'''
self._continue_event.clear()
def resume(self, resume_type):
'''
Resume execution by calling the given resume function.
:param resume_type: Desired resume function identifier.
'''
frame = self._current_frame
# Step into
if resume_type == 1:
self._continue_func = self.set_step
# Step over
elif resume_type == 2:
self._continue_func = functools.partial(self.set_next, frame)
# Step return
elif resume_type == 4:
self._return_hack = True
self._continue_func = functools.partial(self.set_return, frame)
# Continue
else:
self._continue_func = self.set_continue
self._continue_event.set()
def set_continue(self):
'''
FIXME: Override of bdb.Bdb.set_continue
Original stops debugging when no more breakpoints in file.
This means we cannot add breakpoints later on.
'''
self._set_stopinfo(self.botframe, None, -1)
def setBreakpoint(self, breakpoint):
'''
Interface method for Java to have simpler method of setting
a breakpoint.
FIXME: This is a copy of bdb.Bdb.set_break
set_break performs OS level checks to see if file exists.
:param breakpoint: Breakpoint to be set.
'''
filename = self.canonic(breakpoint.getFilename())
lineno = breakpoint.getLineno()
if filename not in self.breaks:
self.breaks[filename] = []
linenos = self.breaks[filename]
if lineno not in linenos:
linenos .append(lineno)
bdb.Breakpoint(filename, lineno, 0, None, None)
def removeBreakpoint(self, breakpoint):
'''
Interface method for Java to have simpler method of removing
a breakpoint.
:param breakpoint: Breakpoint to be removed.
'''
filename = self.canonic(breakpoint.getFilename())
lineno = breakpoint.getLineno()
self.clear_break(filename, lineno)
def stop_here(self, frame):
'''
Hijacks frame check to see if currently executed file has changed.
If file changed new breakpoints will be loaded.
:param frame: Current stack frame to check for file change.
'''
if self._debugger:
file = frame.f_code.co_filename
if file != self._current_file:
self._current_file = file
self.clear_all_file_breaks(file)
breakpoints = self._debugger.getBreakpoints(file)
for breakpoint in breakpoints:
self.setBreakpoint(breakpoint)
# If we have breakpoints we want to let the debugger know
if breakpoints:
return True
return bdb.Bdb.stop_here(self, frame)
def run(self, script, filename):
'''
Executes the script given using the bdb.Bdb.run method.
:param script: Script to be executed.
:param filename: Filename for easier identification of dynamic code.
'''
# Compile code for better inspection
code = '{}\n'.format(script.getCode())
ast_ = ast.parse(code, filename, 'exec')
final_expr = None
for field_ in ast.iter_fields(ast_):
if 'body' != field_[0]:
continue
# Check if last item is an expression
if len(field_[1]) > 0:
if isinstance(field_[1][-1], ast.Expr):
final_expr = ast.Expression()
popped = field_[1].pop(-1)
final_expr.body = popped.value
# Run code up to last expression
return_value = None
compiled = compile(ast_, filename, 'exec')
bdb.Bdb.run(self, compiled)
self._keep_debugging()
# Check if last expression still needs to be run
if final_expr:
compiled = compile(final_expr, filename, 'eval')
return_value = self.runeval(compiled)
self._keep_debugging()
return return_value
def _keep_debugging(self):
'''
Resets debugger state to be able to continue debugging after
script injection.
'''
sys.settrace(self.trace_dispatch)
self.quitting = False
def toString(self):
'''
Required override to avoid problems in py4j bridge.
'''
return str(self)
def equals(self, other):
'''
Required override to avoid problems in py4j bridge.
'''
return self is other
class Java:
implements = ['org.eclipse.ease.lang.python.py4j.internal.ICodeTraceFilter']
# Set up connection between eclipse and python
CodeTracer()