546 lines
20 KiB
Python
Executable File
546 lines
20 KiB
Python
Executable File
"""
|
|
Tracks exceptions raised in Motion Builder and sends email reports about them
|
|
"""
|
|
import os
|
|
import re
|
|
import datetime
|
|
import traceback
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.base import MIMEBase
|
|
from email import encoders
|
|
|
|
import pyfbsdk as mobu
|
|
import pyfbsdk_additions as mobuAdditions
|
|
|
|
import RS.Perforce
|
|
import RS.Config
|
|
import RS.Core.Metadata
|
|
import RS.Utils.Logging
|
|
|
|
SEND_EXCEPTION = True
|
|
SUBJECT = "Motionbuilder {0} - Python Tools Exception".format(RS.Config.Script.TargetBuild)
|
|
|
|
_configMetafile = RS.Core.Metadata.ParseMetaFile(RS.Config.Script.Path.SetupConfig)
|
|
|
|
IgnoreUserList = [ignoreUser.lower() for ignoreUser in _configMetafile.Root.ExceptionConfiguration.IgnoreUserList]
|
|
EmailList = [email.lower() for email in _configMetafile.Root.ExceptionConfiguration.EmailList]
|
|
|
|
SandboxUserList = [user.lower() for user in _configMetafile.Root.SandboxConfiguration.IgnoreUserList]
|
|
SandboxEmailList = [email.lower() for email in _configMetafile.Root.SandboxConfiguration.EmailList]
|
|
|
|
HTML_EXCEPTION_TEMPLATE = "{0}/etc/config/notificationTemplates/ToolsLoggingTemplate.html".format(RS.Config.Tool.Path.TechArt)
|
|
|
|
PastErrorMessages = []
|
|
|
|
IGNORE_DICTIONARY = {"General":
|
|
{"UserList": IgnoreUserList,
|
|
"EmailList": EmailList},
|
|
"Sandbox":
|
|
{"UserList": SandboxUserList,
|
|
"EmailList": SandboxEmailList}
|
|
}
|
|
|
|
BUGSTAR_ON = True
|
|
if BUGSTAR_ON:
|
|
import RS.Utils.Bugstar
|
|
reload(RS.Utils.Bugstar)
|
|
Bugstar = RS.Utils.Bugstar.Bugstar()
|
|
|
|
|
|
|
|
class SlientError(Exception):
|
|
"""
|
|
Raises if not really an error, but aborting sliently for whatever reason
|
|
"""
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Build Info
|
|
|
|
BUILD_INFO = {'10162014-231184': 'HF32',
|
|
'07092014-229896': 'HF26',
|
|
'05262014-229334': 'HF22',
|
|
'02102014-226855': 'HF17',
|
|
'10112013-222115': 'HF12',
|
|
'06262013-218012': 'HF4'}
|
|
|
|
class PropertyError(Exception):
|
|
"""
|
|
Error that has to do with interacting with FBProperties
|
|
"""
|
|
pass
|
|
|
|
def extractInformationFromTraceback(tb):
|
|
"""
|
|
Extracts information from traceback if there is any to extract
|
|
|
|
Arguments:
|
|
tb (traceback): traceback to extract error details from
|
|
|
|
Return:
|
|
string(filename), int(line number), string(method name), string(error string)
|
|
"""
|
|
try:
|
|
filename, lineNum, funcName, text = traceback.extract_tb(tb)[-1]
|
|
|
|
except:
|
|
filename = "Anonymous"
|
|
lineNum = -1
|
|
funcName = "None"
|
|
text = str(tb)
|
|
|
|
return filename, lineNum, funcName, text
|
|
|
|
|
|
def getPaths():
|
|
"""
|
|
Returns the paths to the directories a file must be in for our exception warning to be raised.
|
|
|
|
Return:
|
|
list
|
|
"""
|
|
path_list_string = ""
|
|
format_re_expression = re.compile("(?<={)[a-z.]+(?=})", re.I)
|
|
|
|
# Convert the config xml path values into a string
|
|
path_format_string = "".join(["{},".format(path) for path in _configMetafile.Root.PathsConfiguration.PathsList])
|
|
|
|
# Extract the variable names from the brackets in the path format string
|
|
# ei. we get var from {var}/foo
|
|
path_values_string = "".join("{},".format(value) for value in format_re_expression.findall(path_format_string))
|
|
|
|
# Remove the extracted values in the original string and cancel out special characters
|
|
path_format_string = format_re_expression.sub("", path_format_string)
|
|
path_format_string = re.sub(r"(?<!\\)\\(?=[abfnrtv])", r"\\\\", path_format_string)
|
|
|
|
# Build format method to build paths
|
|
executable_string = "path_list_string = '{}'.format({})".format(path_format_string[:-1], path_values_string[:-1])
|
|
exec executable_string
|
|
path_list = path_list_string.split(",")
|
|
path_list = [os.path.abspath(path) for path in path_list]
|
|
|
|
return path_list
|
|
|
|
|
|
def UnhandledExceptionHandler(exType, exValue, tb):
|
|
"""
|
|
Calls the exception process in a separate definition
|
|
|
|
Arguments:
|
|
exType(Exception): type of exception
|
|
exValue(string): text of the error
|
|
tb(traceback): traceback for the error
|
|
"""
|
|
if isinstance(exValue, SlientError):
|
|
return
|
|
showException(exType, exValue, tb)
|
|
|
|
|
|
# We can call this now without need to crash the tool in session
|
|
|
|
def showException(exType, exValue, tb):
|
|
"""
|
|
Catches unhandled exceptions and will automatically prompt and email the current exception. The exceptions
|
|
are also logged locally in the tools\logs directory.
|
|
|
|
Author:
|
|
Jason Hayes <jason.hayes@rockstarsandiego.com>
|
|
|
|
Note: HB - moved into separate definition so it can be called from menu crashes and not kill the tool.
|
|
Added more information on the email it tries to send.
|
|
"""
|
|
|
|
global SEND_EXCEPTION
|
|
global SUBJECT
|
|
|
|
print 'Unhandled exception caught by Rockstar!'
|
|
|
|
# Print the exception to the Python console.
|
|
|
|
traceback.print_exception(exType, exValue, tb)
|
|
# Extrapolate the exception info. New logic that handles a traceback that is empty.
|
|
filename, lineNum, funcName, text = extractInformationFromTraceback(tb)
|
|
clean_filename = os.path.abspath(filename)
|
|
|
|
ignore_type = ["General", "Sandbox"]["sandboxmotionbuilder" in filename]
|
|
ignore_user = RS.Config.User.Email in IGNORE_DICTIONARY[ignore_type]["UserList"]
|
|
|
|
file_in_correct_directory = [path for path in getPaths() if path in clean_filename]
|
|
|
|
# Only send exception if we aren't in the exception email list, i.e. Technical Artists.
|
|
# and the script lives in one of the tech art repositories i.e. X:\wildwest
|
|
if SEND_EXCEPTION and not ignore_user and file_in_correct_directory:
|
|
|
|
# Get the module name.
|
|
moduleName = str(os.path.basename(filename)).split('.')[0]
|
|
|
|
# Did the exception occur from the console?
|
|
fromConsole = False
|
|
|
|
# Exception occurred from the Python console in Motionbuilder.
|
|
if moduleName == '<MotionBuilder>':
|
|
fromConsole = True
|
|
|
|
# Do not send if the exception occurred from the Motionbuilder Python console.
|
|
if not fromConsole:
|
|
|
|
# Show the exception dialog.
|
|
ShowExceptionDialog([exType, exValue, tb])
|
|
|
|
# Check if the exception has already been raised and reported.
|
|
msgTup = (exValue.__class__, exValue.message, filename, lineNum)
|
|
if msgTup in PastErrorMessages:
|
|
print "Error has already been handled"
|
|
return
|
|
else:
|
|
PastErrorMessages.append(msgTup)
|
|
|
|
# Attempt to email the exception.
|
|
if RS.Config.User.ExchangeServer and RS.Config.User.Email:
|
|
toStr = ''.join(["{};".format(emailAddress) for emailAddress
|
|
in IGNORE_DICTIONARY[ignore_type]["EmailList"]])
|
|
|
|
# Format the exception for email.
|
|
message = MIMEMultipart('alternative')
|
|
message['Subject'] = '{0}: {1}'.format(SUBJECT, os.path.basename(moduleName))
|
|
message['To'] = toStr
|
|
message['From'] = RS.Config.User.Email
|
|
|
|
body = createHtmlException(exType, exValue, tb)
|
|
|
|
htmlBody = MIMEText(body, 'html')
|
|
message.attach(htmlBody)
|
|
|
|
frameData = createFrameDataFileAttachment(tb)
|
|
message.attach(frameData)
|
|
|
|
# Email the exception.
|
|
try:
|
|
server = smtplib.SMTP(RS.Config.User.ExchangeServer)
|
|
server.sendmail(RS.Config.User.Email, IGNORE_DICTIONARY[ignore_type]["EmailList"], message.as_string())
|
|
server.quit()
|
|
|
|
except:
|
|
print 'Could not email the exception to {0}'.format(RS.Config.User.Email)
|
|
else:
|
|
|
|
exception_message = [
|
|
'working outside the tech-art directories',
|
|
'in the exception list']
|
|
|
|
print 'Exception not emailed as user [{}] is {}'.format(RS.Config.User.Email, exception_message[ignore_user])
|
|
|
|
|
|
def createFrameDataFileAttachment(tb):
|
|
frameData = getFrameData(tb)
|
|
|
|
exLogFile = os.path.join(RS.Config.Tool.Path.Logs, "MotionBuilderToolsTraceback.txt")
|
|
|
|
try:
|
|
fstream = open(exLogFile, 'w')
|
|
for line in frameData:
|
|
fstream.write("{0}\n".format(line))
|
|
|
|
print 'Unhandled exception has been logged to ({0}).'.format(exLogFile)
|
|
|
|
except:
|
|
print 'An unknown error occurred trying to write out the exception log to ({0}).'.format(exLogFile)
|
|
|
|
finally:
|
|
fstream.close()
|
|
|
|
part = MIMEBase('application', 'octet-stream')
|
|
part.set_payload(open(exLogFile, 'r').read())
|
|
encoders.encode_base64(part)
|
|
part.add_header('Content-Disposition', 'attachment; filename={0}'.format(os.path.basename(exLogFile)))
|
|
return part
|
|
|
|
|
|
def logToDatabase(dbRawEx, filename, funcName, moduleName, text, p4revision):
|
|
"""
|
|
Logs the error to the database
|
|
|
|
Arguments:
|
|
dbRawEx (string): traceback of the error
|
|
filename (string): name of the file
|
|
funcName (string): name of the method
|
|
moduleName (string): name of the module
|
|
text (string): the error report
|
|
p4revision (int): the perforce revision of the module that errored out
|
|
"""
|
|
try:
|
|
RS.Utils.Logging.Log(moduleName,
|
|
funcName,
|
|
applicationId=RS.Utils.Logging.Application.MotionBuilder,
|
|
sceneFilename=mobu.FBApplication().FBXFileName,
|
|
recordType=RS.Utils.Logging.Type.Error,
|
|
filename=filename,
|
|
perforceRevision=p4revision,
|
|
exception=text,
|
|
traceback=dbRawEx)
|
|
except:
|
|
pass
|
|
|
|
|
|
def getFrameData(tb):
|
|
"""
|
|
Gets all the locals for each frame of the exception level.
|
|
|
|
Example:
|
|
|
|
Frame DeleteSelectionSet in x:\wildwest\dcc\motionbuilder2014\python\RS\Tools\Animation\AnimToFBX\ToolBox.py at line 124
|
|
self = <class 'RS.Tools.Animation.AnimToFBX.ToolBox.ToolBox'> <RS.Tools.Animation.AnimToFBX.ToolBox.ToolBox object at 0x0000000053F77A88>
|
|
|
|
Frame DeleteSelectionSet in x:\wildwest\dcc\motionbuilder2014\python\RS\Tools\Animation\AnimToFBX\Controllers\ToolBox.py at line 207
|
|
setNode = <type 'NoneType'> None
|
|
mainWidget = <class 'RS.Tools.Animation.AnimToFBX.ToolBox.ToolBox'> <RS.Tools.Animation.AnimToFBX.ToolBox.ToolBox object at 0x0000000053F77A88>
|
|
|
|
Arguments:
|
|
tb(traceback): traceback for the error
|
|
|
|
Returns:
|
|
List of Strings, each line being a line of output from the frame in plain text
|
|
"""
|
|
returnList = []
|
|
while tb.tb_next:
|
|
tb = tb.tb_next
|
|
stack = []
|
|
frame = tb.tb_frame
|
|
while frame:
|
|
stack.append(frame)
|
|
frame = frame.f_back
|
|
stack.reverse()
|
|
traceback.print_exc()
|
|
for frame in stack:
|
|
returnList.append("")
|
|
returnList.append("Frame {0} in {1} at line {2}".format(
|
|
frame.f_code.co_name,
|
|
frame.f_code.co_filename,
|
|
frame.f_lineno)
|
|
)
|
|
for key, value in frame.f_locals.items():
|
|
# The Try is to make sure we can convert the object to a string without error
|
|
try:
|
|
strValue = str(value)
|
|
except:
|
|
strValue = "<ERROR WHILE PRINTING VALUE>"
|
|
|
|
if strValue != "":
|
|
returnList.append(" {0} = {1} {2}".format(key, str(type(value)), strValue))
|
|
return returnList
|
|
|
|
|
|
def createHtmlException(exType, exValue, tb):
|
|
"""
|
|
Creates the html representation of the error to send out as an email
|
|
|
|
Arguments:
|
|
exType(Exception): type of exception
|
|
exValue(string): text of the error
|
|
tb(traceback): traceback for the error
|
|
"""
|
|
exRaw = traceback.format_exception(exType, exValue, tb)
|
|
# Swap any newline charactes for html new lines
|
|
for idx in xrange(len(exRaw)):
|
|
exRaw[idx] = re.sub("\\n(?!$)", "<br>", exRaw[idx])
|
|
|
|
filename, lineNum, funcName, text = extractInformationFromTraceback(tb)
|
|
|
|
moduleName = str(os.path.basename(filename)).split('.')[0]
|
|
# get our build version and check if its in the valid dictionary of HF's
|
|
buildVersion = mobu.FBSystem().BuildId.split(' ')[0]
|
|
if buildVersion in BUILD_INFO.keys():
|
|
buildVersion = BUILD_INFO[buildVersion]
|
|
|
|
html_text = None
|
|
haveRevision = 0
|
|
with open(HTML_EXCEPTION_TEMPLATE, 'r') as f:
|
|
html_text = f.read()
|
|
html_text = html_text.replace("#PROJECT#", RS.Config.Project.Name)
|
|
html_text = html_text.replace("#HOTFIX#", buildVersion)
|
|
html_text = html_text.replace("#USERNAME#", RS.Config.User.Name)
|
|
html_text = html_text.replace("#TIMESTAMP#", datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
|
html_text = html_text.replace("#FBXFILENAME#", mobu.FBApplication().FBXFileName)
|
|
html_text = html_text.replace("#PYTHONFILE#", filename)
|
|
try:
|
|
fileInfo = RS.Perforce.GetFileState(filename)
|
|
haveRevision = fileInfo.HaveRevision
|
|
html_text = html_text.replace("#P4REVISION#", "{0} of {1}".format(haveRevision, fileInfo.HeadRevision))
|
|
except:
|
|
html_text = html_text.replace("#P4REVISION#", "")
|
|
|
|
html_text = html_text.replace("#MODULE#", moduleName)
|
|
html_text = html_text.replace("#LINENUMBER#", str(lineNum))
|
|
html_text = html_text.replace("#FUNCTION#", funcName)
|
|
html_text = html_text.replace("#EXCEPTION#", text or "<NO FILE>")
|
|
html_text = html_text.replace("#TRACEBACK#", '<br>'.join(exRaw))
|
|
|
|
# Attempt to log in our database
|
|
logToDatabase(''.join(exRaw), filename, funcName, moduleName, text, haveRevision)
|
|
|
|
return html_text
|
|
|
|
|
|
def LogExceptionToFile(exType, exValue, tb):
|
|
"""
|
|
Creates the log for the exception
|
|
|
|
Arguments:
|
|
exType(Exception): type of exception
|
|
exValue(string): text of the error
|
|
tb(traceback): traceback for the error
|
|
"""
|
|
exLogFile = '{0}\\Rockstar_MotionBuilderToolsException.html'.format(RS.Config.Tool.Path.Logs)
|
|
|
|
html = createHtmlException(exType, exValue, tb)
|
|
|
|
try:
|
|
fstream = open(exLogFile, 'w')
|
|
fstream.write(html)
|
|
|
|
print 'Unhandled exception has been logged to ({0}).'.format(exLogFile)
|
|
|
|
except:
|
|
print 'An unknown error occurred trying to write out the exception log to ({0}).'.format(exLogFile)
|
|
|
|
finally:
|
|
fstream.close()
|
|
|
|
|
|
def SendBug(filename, traceback_error):
|
|
"""
|
|
Creates bug on bugstar and closes error window
|
|
|
|
Arguments:
|
|
filename (string): path to the file that caused the error
|
|
traceback_error (string): the report and traceback error generated by this tool
|
|
"""
|
|
if BUGSTAR_ON:
|
|
CreateBug(filename, traceback_error)
|
|
mobu.CloseToolByName('Rockstar - Tools Error')
|
|
|
|
|
|
def CreateBug(filename, traceback_error):
|
|
"""
|
|
Creates a bug on bugstar based on the traceback error provided
|
|
|
|
Arguments:
|
|
filename (string): the path of the file that caused the error
|
|
traceback_error (string): traceback of the error
|
|
"""
|
|
|
|
# Update to use an XML file later instead of a hardcoded list
|
|
owner = "Mark.Harrison-Ball@rockstargames.com"
|
|
owners = ["Bianca.Rudareanu@rockstarlondon.com", "David.Vega@rockstargames.com"]
|
|
for each in owners:
|
|
if Bugstar.get_user_by_name(each):
|
|
owner = each
|
|
break
|
|
|
|
if not isinstance(traceback, basestring):
|
|
traceback_error = str(traceback_error)
|
|
|
|
user = Bugstar.get_user_by_email(RS.Config.User.Email).Name
|
|
|
|
Bugstar.create_bug("Caught Exception from {python_file} by {user}".format(python_file=filename,
|
|
user=user),
|
|
traceback_error, owner=owner, qaOwner=RS.Config.User.Email)
|
|
print "Error sent "
|
|
|
|
|
|
def ExceptionDialogLayout(mainLayout, exceptionInfo):
|
|
"""
|
|
UI that warns the user that an error just occured
|
|
|
|
Arguments:
|
|
mainLayout (FBLayout): layout to parent the window to
|
|
exceptionInfo (string): details of the error
|
|
"""
|
|
exType, exValue, tb = exceptionInfo
|
|
exRaw = traceback.format_exception(exType, exValue, tb)
|
|
filename, lineNum, funcName, text = extractInformationFromTraceback(tb)
|
|
moduleName = str(os.path.basename(filename)).split('.')[0]
|
|
|
|
lyt = mobuAdditions.FBVBoxLayout()
|
|
|
|
x = mobu.FBAddRegionParam(8, mobu.FBAttachType.kFBAttachLeft, '')
|
|
y = mobu.FBAddRegionParam(-4, mobu.FBAttachType.kFBAttachTop, '')
|
|
w = mobu.FBAddRegionParam(-8, mobu.FBAttachType.kFBAttachRight, '')
|
|
h = mobu.FBAddRegionParam (-8, mobu.FBAttachType.kFBAttachBottom, '')
|
|
|
|
mainLayout.AddRegion('main', 'main', x, y, w, h)
|
|
mainLayout.SetControl('main', lyt)
|
|
|
|
rsImg = mobu.FBImageContainer()
|
|
rsImg.Filename = '{0}\\rsLogo.png'.format(RS.Config.Script.Path.ToolImages)
|
|
|
|
lbl1 = mobu.FBLabel()
|
|
|
|
# Toronto mail servers block email.
|
|
if RS.Config.User.ExchangeServer is None:
|
|
lbl1.Caption = ('An unhandled exception was caught by Rockstar! '
|
|
'Please email the following information to support.')
|
|
|
|
else:
|
|
lbl1.Caption = ('An unhandled exception was caught by Rockstar! '
|
|
'The information below has been automatically emailed to support.')
|
|
|
|
output = mobu.FBMemo()
|
|
s1 = mobu.FBStringList()
|
|
error_list = []
|
|
|
|
exStr = ''
|
|
for exLine in exRaw:
|
|
exStr += exLine
|
|
|
|
error_list.append('Username: {0}\n'.format(RS.Config.User.Name))
|
|
error_list.append('Timestamp: {0}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
|
error_list.append('FBX Filename: {0}\n'.format(mobu.FBApplication().FBXFileName))
|
|
error_list.append('Python Filename: {0}\n'.format(filename))
|
|
|
|
try:
|
|
fileInfo = RS.Perforce.GetFileState(filename)
|
|
error_list.append('Perforce Revision: {0} of {1}\n'.format(fileInfo.HaveRevision, fileInfo.HeadRevision))
|
|
|
|
except:
|
|
error_list.append('Perforce Revision: None\n')
|
|
|
|
error_list.append('Module: {0}\n'.format(moduleName))
|
|
error_list.append('Line Number: {0}\n'.format(lineNum))
|
|
error_list.append('Function: {0}\n'.format(funcName))
|
|
error_list.append('Exception: {0}\n'.format(text))
|
|
error_list.append('\n')
|
|
error_list.append('{0}\n'.format(exStr))
|
|
|
|
[s1.Add(each[:-1]) for each in error_list]
|
|
|
|
output.SetStrings(s1)
|
|
|
|
b = mobu.FBButton()
|
|
b.Caption = "Send to BugStar!"
|
|
b.Justify = mobu.FBTextJustify.kFBTextJustifyCenter
|
|
b.OnClick.Add(lambda *args:SendBug(filename, "".join(error_list)))
|
|
|
|
lyt.Add(lbl1, 20)
|
|
lyt.AddRelative(output, 1.0)
|
|
lyt.Add(b, 60)
|
|
lyt.Add(rsImg, 50)
|
|
|
|
|
|
def ShowExceptionDialog(exceptionInfo):
|
|
"""
|
|
Shows the UI that warns the user that an error just occured
|
|
|
|
Arguments:
|
|
exceptionInfo (string): details of the error
|
|
"""
|
|
t = mobuAdditions.FBCreateUniqueTool('Rockstar - Tools Error')
|
|
t.StartSizeX = 600
|
|
t.StartSizeY = 400
|
|
ExceptionDialogLayout(t, exceptionInfo)
|
|
mobu.ShowTool(t)
|