Files
2025-09-29 00:52:08 +02:00

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)