274 lines
9.3 KiB
Python
274 lines
9.3 KiB
Python
"""Exception and error handling.
|
|
|
|
This contains the core exceptions that the implementations should raise
|
|
as well as the IActiveScriptError interface code.
|
|
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
import traceback
|
|
|
|
import pythoncom
|
|
import win32com.server.exception
|
|
import win32com.server.util
|
|
import winerror
|
|
from win32com.axscript import axscript
|
|
|
|
debugging = 0
|
|
|
|
|
|
def FormatForAX(text):
|
|
"""Format a string suitable for an AX Host"""
|
|
# Replace all " with ', so it works OK in HTML (ie, ASP)
|
|
return ExpandTabs(AddCR(text))
|
|
|
|
|
|
def ExpandTabs(text):
|
|
return re.sub("\t", " ", text)
|
|
|
|
|
|
def AddCR(text):
|
|
return re.sub("\n", "\r\n", text)
|
|
|
|
|
|
class IActiveScriptError:
|
|
"""An implementation of IActiveScriptError
|
|
|
|
The ActiveX Scripting host calls this client whenever we report
|
|
an exception to it. This interface provides the exception details
|
|
for the host to report to the user.
|
|
"""
|
|
|
|
_com_interfaces_ = [axscript.IID_IActiveScriptError]
|
|
_public_methods_ = ["GetSourceLineText", "GetSourcePosition", "GetExceptionInfo"]
|
|
|
|
def _query_interface_(self, iid):
|
|
print("IActiveScriptError QI - unknown IID", iid)
|
|
return 0
|
|
|
|
def _SetExceptionInfo(self, exc):
|
|
self.exception = exc
|
|
|
|
def GetSourceLineText(self):
|
|
return self.exception.linetext
|
|
|
|
def GetSourcePosition(self):
|
|
ctx = self.exception.sourceContext
|
|
# Zero based in the debugger (but our columns are too!)
|
|
return (
|
|
ctx,
|
|
self.exception.lineno + self.exception.startLineNo - 1,
|
|
self.exception.colno,
|
|
)
|
|
|
|
def GetExceptionInfo(self):
|
|
return self.exception
|
|
|
|
|
|
class AXScriptException(win32com.server.exception.COMException):
|
|
"""A class used as a COM exception.
|
|
|
|
Note this has attributes which conform to the standard attributes
|
|
for COM exceptions, plus a few others specific to our IActiveScriptError
|
|
object.
|
|
"""
|
|
|
|
def __init__(self, site, codeBlock, exc_type, exc_value, exc_traceback):
|
|
# set properties base class shares via base ctor...
|
|
win32com.server.exception.COMException.__init__(
|
|
self,
|
|
description="Unknown Exception",
|
|
scode=winerror.DISP_E_EXCEPTION,
|
|
source="Python ActiveX Scripting Engine",
|
|
)
|
|
|
|
# And my other values...
|
|
if codeBlock is None:
|
|
self.sourceContext = 0
|
|
self.startLineNo = 0
|
|
else:
|
|
self.sourceContext = codeBlock.sourceContextCookie
|
|
self.startLineNo = codeBlock.startLineNumber
|
|
self.linetext = ""
|
|
|
|
self.__BuildFromException(site, exc_type, exc_value, exc_traceback)
|
|
|
|
def __BuildFromException(self, site, type, value, tb):
|
|
if debugging:
|
|
import linecache
|
|
|
|
linecache.clearcache()
|
|
try:
|
|
if issubclass(type, SyntaxError):
|
|
self._BuildFromSyntaxError(site, value, tb)
|
|
else:
|
|
self._BuildFromOther(site, type, value, tb)
|
|
except: # Error extracting traceback info!!!
|
|
traceback.print_exc()
|
|
# re-raise.
|
|
raise
|
|
|
|
def _BuildFromSyntaxError(self, site, exc, tb):
|
|
value = exc.args
|
|
# All syntax errors should have a message as element 0
|
|
try:
|
|
msg = value[0]
|
|
except:
|
|
msg = "Unknown Error (%s)" % (value,)
|
|
try:
|
|
(filename, lineno, offset, line) = value[1]
|
|
# Some of these may be None, which upsets us!
|
|
if offset is None:
|
|
offset = 0
|
|
if line is None:
|
|
line = ""
|
|
except:
|
|
msg = "Unknown"
|
|
lineno = 0
|
|
offset = 0
|
|
line = "Unknown"
|
|
self.description = FormatForAX(msg)
|
|
self.lineno = lineno
|
|
self.colno = offset - 1
|
|
self.linetext = ExpandTabs(line.rstrip())
|
|
|
|
def _BuildFromOther(self, site, exc_type, value, tb):
|
|
self.colno = -1
|
|
self.lineno = 0
|
|
if debugging: # Full traceback if debugging.
|
|
list = traceback.format_exception(exc_type, value, tb)
|
|
self.description = ExpandTabs("".join(list))
|
|
return
|
|
# Run down the traceback list, looking for the first "<Script..>"
|
|
# Hide traceback above this. In addition, keep going down
|
|
# looking for a "_*_" attribute, and below hide these also.
|
|
hide_names = [
|
|
"r_import",
|
|
"r_reload",
|
|
"r_open",
|
|
] # hide from these functions down in the traceback.
|
|
depth = None
|
|
tb_top = tb
|
|
while tb_top:
|
|
filename, lineno, name, line = self.ExtractTracebackInfo(tb_top, site)
|
|
if filename[:7] == "<Script":
|
|
break
|
|
tb_top = tb_top.tb_next
|
|
format_items = []
|
|
if tb_top: # found one.
|
|
depth = 0
|
|
tb_look = tb_top
|
|
# Look down for our bottom
|
|
while tb_look:
|
|
filename, lineno, name, line = self.ExtractTracebackInfo(tb_look, site)
|
|
if name in hide_names:
|
|
break
|
|
# We can report a line-number, but not a filename. Therefore,
|
|
# we return the last line-number we find in one of our script
|
|
# blocks.
|
|
if filename.startswith("<Script"):
|
|
self.lineno = lineno
|
|
self.linetext = line
|
|
format_items.append((filename, lineno, name, line))
|
|
depth = depth + 1
|
|
tb_look = tb_look.tb_next
|
|
else:
|
|
depth = None
|
|
tb_top = tb
|
|
|
|
bits = ["Traceback (most recent call last):\n"]
|
|
bits.extend(traceback.format_list(format_items))
|
|
if exc_type == pythoncom.com_error:
|
|
desc = "%s (0x%x)" % (value.strerror, value.hresult)
|
|
if (
|
|
value.hresult == winerror.DISP_E_EXCEPTION
|
|
and value.excepinfo
|
|
and value.excepinfo[2]
|
|
):
|
|
desc = value.excepinfo[2]
|
|
bits.append("COM Error: " + desc)
|
|
else:
|
|
bits.extend(traceback.format_exception_only(exc_type, value))
|
|
|
|
# XXX - this utf8 encoding seems bogus. From well before py3k,
|
|
# we had the comment:
|
|
# > all items in the list are utf8 courtesy of Python magically
|
|
# > converting unicode to utf8 before compilation.
|
|
# but that is likely just confusion from early unicode days;
|
|
# Python isn't doing it, pywin32 probably was, so 'mbcs' would
|
|
# be the default encoding. We should never hit this these days
|
|
# anyway, but on py3k, we *never* will, and str objects there
|
|
# don't have a decode method...
|
|
if sys.version_info < (3,):
|
|
for i in range(len(bits)):
|
|
if type(bits[i]) is str:
|
|
# assert type(bits[i]) is str, type(bits[i])
|
|
bits[i] = bits[i].decode("utf8")
|
|
|
|
self.description = ExpandTabs("".join(bits))
|
|
# Clear tracebacks etc.
|
|
tb = tb_top = tb_look = None
|
|
|
|
def ExtractTracebackInfo(self, tb, site):
|
|
import linecache
|
|
|
|
f = tb.tb_frame
|
|
lineno = tb.tb_lineno
|
|
co = f.f_code
|
|
filename = co.co_filename
|
|
name = co.co_name
|
|
line = linecache.getline(filename, lineno)
|
|
if not line:
|
|
try:
|
|
codeBlock = site.scriptCodeBlocks[filename]
|
|
except KeyError:
|
|
codeBlock = None
|
|
if codeBlock:
|
|
# Note: 'line' will now be unicode.
|
|
line = codeBlock.GetLineNo(lineno)
|
|
if line:
|
|
line = line.strip()
|
|
else:
|
|
line = None
|
|
return filename, lineno, name, line
|
|
|
|
def __repr__(self):
|
|
return "AXScriptException Object with description:" + self.description
|
|
|
|
|
|
def ProcessAXScriptException(scriptingSite, debugManager, exceptionInstance):
|
|
"""General function to handle any exception in AX code
|
|
|
|
This function creates an instance of our IActiveScriptError interface, and
|
|
gives it to the host, along with out exception class. The host will
|
|
likely call back on the IActiveScriptError interface to get the source text
|
|
and other information not normally in COM exceptions.
|
|
"""
|
|
# traceback.print_exc()
|
|
instance = IActiveScriptError()
|
|
instance._SetExceptionInfo(exceptionInstance)
|
|
gateway = win32com.server.util.wrap(instance, axscript.IID_IActiveScriptError)
|
|
if debugManager:
|
|
fCallOnError = debugManager.HandleRuntimeError()
|
|
if not fCallOnError:
|
|
return None
|
|
|
|
try:
|
|
result = scriptingSite.OnScriptError(gateway)
|
|
except pythoncom.com_error as details:
|
|
print("**OnScriptError failed:", details)
|
|
print("Exception description:'%s'" % (repr(exceptionInstance.description)))
|
|
print("Exception text:'%s'" % (repr(exceptionInstance.linetext)))
|
|
result = winerror.S_FALSE
|
|
|
|
if result == winerror.S_OK:
|
|
# If the above returns NOERROR, it is assumed the error has been
|
|
# correctly registered and the value SCRIPT_E_REPORTED is returned.
|
|
ret = win32com.server.exception.COMException(scode=axscript.SCRIPT_E_REPORTED)
|
|
return ret
|
|
else:
|
|
# The error is taken to be unreported and is propagated up the call stack
|
|
# via the IDispatch::Invoke's EXCEPINFO parameter (hr returned is DISP_E_EXCEPTION.
|
|
return exceptionInstance
|