""" 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"(? 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 == '': 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 = Frame DeleteSelectionSet in x:\wildwest\dcc\motionbuilder2014\python\RS\Tools\Animation\AnimToFBX\Controllers\ToolBox.py at line 207 setNode = None mainWidget = 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 = "" 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(?!$)", "
", 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 "") html_text = html_text.replace("#TRACEBACK#", '
'.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)