-- ------------------------------------------------------------------------------ -- -- TradeSkillMaster -- -- https://tradeskillmaster.com -- -- All Rights Reserved - Detailed license information included with addon. -- -- ------------------------------------------------------------------------------ -- -- TSM's error handler local TSM = select(2, ...) ---@type TSM local ErrorHandler = TSM.Init("Service.ErrorHandler") local Log = TSM.Include("Util.Log") local String = TSM.Include("Util.String") local Event = TSM.Include("Util.Event") local JSON = TSM.Include("Util.JSON") local TempTable = TSM.Include("Util.TempTable") local L = TSM.Include("Locale").GetTable() local LibTSMClass = TSM.Include("LibTSMClass") local private = { origErrorHandler = nil, errorFrame = nil, isSilent = nil, errorSuppressed = nil, errorReports = {}, num = 0, localLinesTemp = {}, hitInternalError = false, isManual = nil, ignoreErrors = false, globalNameTranslation = {}, } local MAX_ERROR_REPORT_AGE = 7 * 24 * 60 * 60 -- 1 week local MAX_STACK_DEPTH = 50 local ADDON_SUITES = { "ArkInventory", "AtlasLoot", "Altoholic", "Auc-", "Bagnon", "BigWigs", "Broker", "ButtonFacade", "Carbonite", "DataStore", "DBM", "Dominos", "DXE", "EveryQuest", "Forte", "FuBar", "GatherMate2", "Grid", "LightHeaded", "LittleWigs", "Masque", "MogIt", "Odyssey", "Overachiever", "PitBull4", "Prat-3.0", "RaidAchievement", "Skada", "SpellFlash", "TidyPlates", "TipTac", "Titan", "UnderHood", "WowPro", "ZOMGBuffs", } local PRINT_PREFIX = "|cffff0000TSM:|r " -- ============================================================================ -- Module Functions -- ============================================================================ function ErrorHandler.ShowForThread(err, thread) -- show an error, but don't cause an exception to be thrown private.isSilent = true private.ErrorHandler(err, thread) end function ErrorHandler.ShowManual() private.isManual = true -- show an error, but don't cause an exception to be thrown private.isSilent = true private.ErrorHandler("Manually triggered error") end function ErrorHandler.SaveReports(appDB) if private.errorFrame then private.errorFrame:Hide() end appDB.errorReports = appDB.errorReports or { updateTime = 0, data = {} } if #private.errorReports > 0 then appDB.errorReports.updateTime = private.errorReports[#private.errorReports].timestamp end -- remove any events which are too old for i = #appDB.errorReports.data, 1, -1 do local timestamp = strmatch(appDB.errorReports.data[i], "([0-9]+)%]$") or "" if (tonumber(timestamp) or 0) < time() - MAX_ERROR_REPORT_AGE then tremove(appDB.errorReports.data, i) end end for _, report in ipairs(private.errorReports) do local line = format("[%s,\"%s\",%d]", JSON.Encode(report.errorInfo), report.details, report.timestamp) tinsert(appDB.errorReports.data, line) end end -- ============================================================================ -- Error Handler -- ============================================================================ function private.ErrorHandler(msg, thread) -- ignore errors while we are handling this error private.ignoreErrors = true local isSilent = private.isSilent private.isSilent = nil local isManual = private.isManual private.isManual = nil private.CreateErrorFrame() if type(thread) ~= "thread" then thread = nil end if private.errorFrame:IsVisible() and private.errorSuppressed then -- already showing an error and suppressed another one, so silently ignore this one private.ignoreErrors = false return true end -- shorten the paths in the error message msg = gsub(msg, "%.%.%.T?r?a?d?e?S?k?i?l?l?M?a?ster([_A-Za-z]*[\\/])", "TradeSkillMaster%1") msg = strsub(msg, strfind(msg, "TradeSkillMaster") or 1) msg = gsub(msg, "TradeSkillMaster([^%.])", "TSM%1") -- build our global name translation table wipe(private.globalNameTranslation) pcall(function() local UIElements = TSM.Include("UI.UIElements") local temp = {} UIElements.GetDebugNameTranslation(temp) for k, v in pairs(temp) do private.globalNameTranslation[String.Escape(k)] = v end end) -- build stack trace with locals and get addon name local stackInfo, newMsg = private.GetStackInfo(msg, thread) msg = newMsg local addonName = isSilent and "TradeSkillMaster" or nil for _, info in ipairs(stackInfo) do if not addonName then addonName = strmatch(info.file, "[A-Za-z]+%.lua") and private.IsTSMAddon(info.file) or nil end end if not isManual and addonName ~= "TradeSkillMaster" then -- not a TSM error private.ignoreErrors = false return false end if not TSM.IsDevVersion() and not isManual then -- log the error (use a format string in case there are '%' characters in the msg) Log.Err("%s", msg) end if private.errorFrame:IsVisible() then -- already showing an error, so suppress this one and return private.errorSuppressed = true print(PRINT_PREFIX..L["Additional error suppressed"]) return true end private.num = private.num + 1 local clientVersion, clientBuild = GetBuildInfo() local errorInfo = { msg = #stackInfo > 0 and gsub(msg, String.Escape(stackInfo[1].file)..":"..stackInfo[1].line..": ", "") or msg, stackInfo = stackInfo, time = time(), debugTime = floor(debugprofilestop()), client = format("%s (%s)", clientVersion, clientBuild), locale = GetLocale(), inCombat = tostring(InCombatLockdown() and true or false), version = TSM.GetVersion(), } -- temp table info local tempTableLines = {} for _, info in ipairs(TempTable.GetDebugInfo()) do tinsert(tempTableLines, info) end errorInfo.tempTableStr = table.concat(tempTableLines, "\n") -- object pool info local status, objectPoolInfo = pcall(function() return TSM.Include("Util.ObjectPool").GetDebugInfo() end) local objectPoolLines = {} if status then for name, objectInfo in pairs(objectPoolInfo) do tinsert(objectPoolLines, format("%s (%d created, %d in use)", name, objectInfo.numCreated, objectInfo.numInUse)) for _, info in ipairs(objectInfo.info) do tinsert(objectPoolLines, " "..info) end end end errorInfo.objectPoolStr = status and table.concat(objectPoolLines, "\n") or tostring(objectPoolInfo) -- TSM thread info local threadInfoStr = nil status, threadInfoStr = pcall(function() return TSM.Include("Service.Threading").GetDebugStr() end) errorInfo.threadInfoStr = tostring(threadInfoStr) -- recent debug log entries local entries = {} for i = Log.Length(), 1, -1 do local severity, location, timeStr, logMsg = Log.Get(i) tinsert(entries, format("%s [%s] {%s} %s", timeStr, severity, location, logMsg)) end errorInfo.debugLogStr = table.concat(entries, "\n") -- addons local hasAddonSuite = {} local addonsLines = {} for i = 1, GetNumAddOns() do local name, _, _, loadable = GetAddOnInfo(i) if loadable then local version = strtrim(GetAddOnMetadata(name, "X-Curse-Packaged-Version") or GetAddOnMetadata(name, "Version") or "") local loaded = IsAddOnLoaded(i) local isSuite = nil for _, commonTerm in ipairs(ADDON_SUITES) do if strsub(name, 1, #commonTerm) == commonTerm then isSuite = commonTerm break end end local commonTerm = "TradeSkillMaster" if isSuite then if not hasAddonSuite[isSuite] then tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]")) hasAddonSuite[isSuite] = true end elseif strsub(name, 1, #commonTerm) == commonTerm then name = gsub(name, "TradeSkillMaster", "TSM") tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]")) else tinsert(addonsLines, name.." ("..version..")"..(loaded and "" or " [Not Loaded]")) end end end errorInfo.addonsStr = table.concat(addonsLines, "\n") -- show this error local stackInfoLines = {} for _, info in ipairs(errorInfo.stackInfo) do local localsStr = info.localsStr ~= "" and ("\n |cffaaaaaa"..gsub(info.localsStr, "\n", "\n ").."|r") or "" local locationStr = info.line ~= 0 and strjoin(":", info.file, info.line) or info.file tinsert(stackInfoLines, locationStr.." <"..info.func..">"..localsStr) end private.errorFrame.errorStr = strjoin("\n", private.FormatErrorMessageSection("Message", msg), private.FormatErrorMessageSection("Time", date("%m/%d/%y %H:%M:%S", errorInfo.time).." ("..floor(errorInfo.debugTime)..")"), private.FormatErrorMessageSection("Client", errorInfo.client), private.FormatErrorMessageSection("Locale", errorInfo.locale), private.FormatErrorMessageSection("Combat", errorInfo.inCombat), private.FormatErrorMessageSection("Error Count", private.num), private.FormatErrorMessageSection("Stack Trace", table.concat(stackInfoLines, "\n"), true), private.FormatErrorMessageSection("Temp Tables", errorInfo.tempTableStr, true), private.FormatErrorMessageSection("Object Pools", errorInfo.objectPoolStr, true), private.FormatErrorMessageSection("Running Threads", errorInfo.threadInfoStr, true), private.FormatErrorMessageSection("Debug Log", errorInfo.debugLogStr, true), private.FormatErrorMessageSection("Addons", errorInfo.addonsStr, true) ) -- remove unprintable characters private.errorFrame.errorStr = gsub(private.errorFrame.errorStr, "[%z\001-\008\011-\031]", "?") private.errorFrame.errorInfo = errorInfo private.errorFrame.isManual = isManual private.errorFrame:Show() print(PRINT_PREFIX..L["Looks like TradeSkillMaster has encountered an error. Please help the author fix this error by following the instructions shown."]) if TSM.IsTestEnvironment() then print(private.errorFrame.errorStr) end private.ignoreErrors = false return true end -- ============================================================================ -- Private Helper Functions -- ============================================================================ function private.GetStackInfo(msg, thread) local errLocation = strmatch(msg, "[A-Za-z]+%.lua:[0-9]+") local stackFrames = private.GetStackFrames(thread) local startIndex = nil for i, frame in ipairs(stackFrames) do local prevFrame = stackFrames[i-1] if prevFrame and strfind(frame.file, "LibTSMClass%.lua") then -- TODO: Ignore stack frames from the class code's wrapper function if frame.func ~= "?" and prevFrame.func and not strmatch(frame.func, "^.+:[0-9]+$") and strmatch(prevFrame.func, "^.+:[0-9]+$") then -- This stack frame includes the class method we were accessing in the previous one, so go back and fix it up if frame.rawLocals then local className, objKey = strmatch(frame.rawLocals, "\n +str = \"([A-Za-z_0-9]+):([0-9A-F]+)\"\n") if className then if TSM.IsDevVersion() then prevFrame.localsStr = prevFrame.localsStr.."\n"..LibTSMClass.GetDebugInfo(className..":"..objKey, 5, private.LocalTableLookupFunc) end prevFrame.func = className.."."..frame.func else prevFrame.func = "?."..frame.func end else prevFrame.func = "?."..frame.func end end end if not startIndex then if errLocation and strmatch(frame.file..":"..frame.line, "[A-Za-z]+%.lua:[0-9]+") == errLocation then startIndex = strfind(frame.file, "LibTSMClass%.lua") and (i - 1) or i elseif not errLocation and i > (thread and 1 or 4) and frame.file ~= "[C]" then startIndex = i end end end if not startIndex then return {} end -- Remove the extra frames from the top for _ = 1, startIndex - 1 do tremove(stackFrames, 1) end -- Fix up the error message if errLocation and strfind(errLocation, "LibTSMClass%.lua:%d+") and stackFrames[1] and not strfind(stackFrames[1].file, "LibTSMClass%.lua") then msg = gsub(msg, ".+LibTSMClass%.lua:[0-9]+", stackFrames[1].file..":"..stackFrames[1].line) end return stackFrames, msg end function private.GetStackFrames(thread) local stackFrames = {} local consecutiveIgnored = nil for i = 0, math.huge do local file, line, func, locals = private.GetStackFrame(i, thread) if file then tinsert(stackFrames, { file = file, line = line, func = func, rawLocals = locals, localsStr = locals and private.ParseLocals(locals, file) or "", }) consecutiveIgnored = 0 else consecutiveIgnored = consecutiveIgnored + 1 if consecutiveIgnored >= 20 or #stackFrames >= MAX_STACK_DEPTH then break end end end return stackFrames end function private.GetStackFrame(level, thread) local stackLine = nil if thread then stackLine = debugstack(thread, level, 1, 0) else level = level + 1 stackLine = debugstack(level, 1, 0) end stackLine = gsub(stackLine, "^%[string \"@([^%.]+%.lua)\"%]", "%1") local locals = debuglocals(level) stackLine = gsub(stackLine, "%.%.%.T?r?a?d?e?S?k?i?l?l?M?a?ster([_A-Za-z]*[\\/])", "TradeSkillMaster%1") stackLine = gsub(stackLine, "%.%.%.", "") stackLine = gsub(stackLine, "`", "<", 1) stackLine = gsub(stackLine, "'", ">", 1) stackLine = strtrim(stackLine) if stackLine == "" then return end -- Parse out the file, line, and function name local locationStr, functionStr = strmatch(stackLine, "^(.-): in function (<[^\n]*>)") if not locationStr then locationStr, functionStr = strmatch(stackLine, "^(.-): in (main chunk)") end if not locationStr then return end locationStr = strsub(locationStr, strfind(locationStr, "TradeSkillMaster") or 1) locationStr = gsub(locationStr, "TradeSkillMaster([^%.])", "TSM%1") functionStr = functionStr and gsub(gsub(functionStr, ".*[\\/]", ""), "[<>]", "") or "" local file, line = strmatch(locationStr, "^(.+):(%d+)$") file = file or locationStr line = tonumber(line) or 0 local func = strsub(functionStr, strfind(functionStr, "`") and 2 or 1, -1) or "?" func = func ~= "" and func or "?" return file, line, func, locals end function private.ParseLocals(locals, file) if strmatch(file, "^%[") then return end local fileName = strmatch(file, "([A-Za-z%-_0-9]+)%.lua") local isBlizzardFile = strmatch(file, "Interface[\\/]FrameXML[\\/]") local isPrivateTable, isLocaleTable, isPackageTable, isSelfTable, isTemporaryTable = false, false, false, false, false wipe(private.localLinesTemp) locals = gsub(locals, "<([a-z]+)> {[\n\t ]+}", "<%1> {}") locals = gsub(locals, " = defined @", "@") locals = gsub(locals, " {", "{") local level = 0 for localLine in gmatch(locals, "[^\n]+") do local shouldIgnoreLine = false if strmatch(localLine, "^ *%(%*temporary%) = nil") then -- ignore nil temporary variables shouldIgnoreLine = true elseif strmatch(localLine, "LibTSMClass%.lua:") then -- ignore class methods shouldIgnoreLine = true elseif strmatch(localLine, " {}$") then -- ignore internal WoW frame members shouldIgnoreLine = true elseif strtrim(localLine) == "" then -- ignore empty lines shouldIgnoreLine = true elseif strmatch(localLine, "^ += .+$") then -- ignore lines which start with a '=' shouldIgnoreLine = true elseif strmatch(localLine, "= <([^>]+)$") then -- ignore lines with unmatched '<', '>' in their value shouldIgnoreLine = true end if not shouldIgnoreLine then level = #strmatch(localLine, "^ *") localLine = strrep(" ", level)..strtrim(localLine) localLine = gsub(localLine, "Interface[\\/][Aa]dd[Oo]ns[\\/]TradeSkillMaster", "TSM") localLine = gsub(localLine, "\124", "\\124") for matchStr, replaceStr in pairs(private.globalNameTranslation) do localLine = gsub(localLine, matchStr, replaceStr) end if level > 0 then if isBlizzardFile then -- for Blizzard stack frames, only include level 0 locals shouldIgnoreLine = true elseif strmatch(localLine, "^ *[_]*[A-Z].+@TSM") then -- ignore table methods (based on their name being UpperCamelCase - potentially with leading underscores) shouldIgnoreLine = true elseif strmatch(localLine, "^ *[_]*[A-Z].+@") and not strmatch(localLine, ":%d+$") then -- ignore cut-off table method lines shouldIgnoreLine = true elseif isLocaleTable then -- ignore everything within the locale table shouldIgnoreLine = true elseif isPackageTable then -- ignore the package table completely shouldIgnoreLine = true elseif isTemporaryTable then -- Ignore temporary tables completely shouldIgnoreLine = true elseif (isSelfTable or isPrivateTable) and strmatch(localLine, "^ *[_a-zA-Z0-9]+ = {}") then -- ignore empty tables within objects or the private table shouldIgnoreLine = true elseif strmatch(localLine, "^%s+0 = $") then -- remove userdata table entries shouldIgnoreLine = true end end if not shouldIgnoreLine then tinsert(private.localLinesTemp, localLine) end if level == 0 then isPackageTable = strmatch(localLine, "%s*"..fileName.." = {") and true or false isPrivateTable = strmatch(localLine, "%s*private = {") and true or false isLocaleTable = strmatch(localLine, "%s*L = {") and true or false isSelfTable = strmatch(localLine, "%s*self = {") and true or false isTemporaryTable = strmatch(localLine, "%s*%(%*temporary%) =.*{") and true or false end end end -- add closing brackets for tables which got cut off at the end while level > 0 do level = level - 1 tinsert(private.localLinesTemp, strrep(" ", level).."}") end -- remove any top-level empty tables local i = #private.localLinesTemp while i > 0 do if i > 1 and private.localLinesTemp[i] == "}" and strmatch(private.localLinesTemp[i - 1], "^[A-Za-z_%(].* = {$") then tremove(private.localLinesTemp, i) tremove(private.localLinesTemp, i - 1) i = i - 2 elseif strmatch(private.localLinesTemp[i], "^[A-Za-z_%(].* = {}$") then tremove(private.localLinesTemp, i) i = i - 1 elseif i > 1 and private.localLinesTemp[i] == "}" and strmatch(private.localLinesTemp[i - 1], "^[A-Za-z_%(].* =.*{$") then -- Don't remove this table, but collapse it private.localLinesTemp[i - 1] = private.localLinesTemp[i - 1].."}" tremove(private.localLinesTemp, i) i = i - 1 else i = i - 1 end end return #private.localLinesTemp > 0 and table.concat(private.localLinesTemp, "\n") or nil end function private.LocalTableLookupFunc(tbl) local status, result = pcall(function() return TSM.Include("Util.ReactiveClasses.State").GetDebugInfo(tbl) end) return status and result ~= "" and result or nil end function private.IsTSMAddon(str) if strfind(str, "Auc-Adcanced[\\/]CoreScan.lua") then -- ignore auctioneer errors return nil elseif strfind(str, "Master[\\/]External[\\/]") then -- ignore errors from libraries return nil elseif strfind(str, "Master[\\/]Core[\\/]API.lua") then -- ignore errors from public APIs return nil elseif strfind(str, "Master_AppHelper[\\/]") then return "TradeSkillMaster_AppHelper" elseif strfind(str, "lMaster[\\/]") then return "TradeSkillMaster" elseif strfind(str, "ster[\\/]Core[\\/]UI[\\/]") then return "TradeSkillMaster" elseif strfind(str, "r[\\/]LibTSM[\\/]") then return "TradeSkillMaster" elseif strfind(str, "^TSM[\\/]") then return "TradeSkillMaster" end return nil end function private.AddonBlockedHandler(event, addonName, addonFunc) if not strmatch(addonName, "TradeSkillMaster") then return end -- just log it - it might not be TSM that cause the taint Log.Err("[%s] AddOn '%s' tried to call the protected function '%s'.", event, addonName or "", addonFunc or "") if TSM.IsDevVersion() then local status, ret = pcall(private.ErrorHandler, "BLOCKED") if not status and not private.hitInternalError then private.hitInternalError = true print("Internal TSM error: "..tostring(ret)) end end end function private.SanitizeString(str) str = gsub(str, "\124cff[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]([^\124]+)\124r", "%1") str = gsub(str, "[\\]+", "/") str = gsub(str, "\"", "'") return str end function private.FormatErrorMessageSection(heading, info, isMultiLine) -- replace unprintable characters with "?" info = gsub(info, "[^\t\n -~]", "?") local prefix = nil if isMultiLine then prefix = info ~= "" and "\n " or "" info = gsub(info, "\n", "\n ") else prefix = info ~= "" and " " or "" end return "|cff99ffff"..heading..":|r"..prefix..info end -- ============================================================================ -- Error Frame -- ============================================================================ function private.CreateErrorFrame() if private.errorFrame then return end local STEPS_TEXT = "Steps leading up to the error:\n1) List\n2) Steps\n3) Here" local frame = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate" or nil) private.errorFrame = frame frame:Hide() frame:SetWidth(500) frame:SetHeight(400) frame:SetFrameStrata("FULLSCREEN_DIALOG") frame:SetPoint("RIGHT", -100, 0) frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 2, }) frame:SetBackdropColor(0, 0, 0, 1) frame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1) frame:SetScript("OnShow", function(self) self.showingError = self.isManual or TSM.IsDevVersion() self.details = STEPS_TEXT if self.showingError then -- this is a dev version so show the error (only) self.text:SetText("Looks like TradeSkillMaster has encountered an error.") self.switchBtn:SetText("Hide Error") self.editBox:SetText(self.errorStr) else self.text:SetText("Looks like TradeSkillMaster has encountered an error. Please provide the steps which lead to this error to help the TSM team fix it, then click either button at the bottom of the window to automatically report this error.") self.switchBtn:SetText("Show Error") self.editBox:SetText(self.details) end end) frame:SetScript("OnHide", function() local details = private.errorFrame.showingError and private.errorFrame.details or private.errorFrame.editBox:GetText() local changedDetails = details ~= STEPS_TEXT if (not TSM.IsDevVersion() and not private.errorFrame.isManual and (changedDetails or private.num == 1)) or IsShiftKeyDown() then tinsert(private.errorReports, { errorInfo = private.errorFrame.errorInfo, details = private.SanitizeString(details), timestamp = time(), }) end private.errorSuppressed = nil end) local title = frame:CreateFontString() title:SetHeight(20) title:SetPoint("TOPLEFT", 0, -10) title:SetPoint("TOPRIGHT", 0, -10) title:SetFontObject(GameFontNormalLarge) title:SetTextColor(1, 1, 1, 1) title:SetJustifyH("CENTER") title:SetJustifyV("MIDDLE") local status, versionText = pcall(TSM.GetVersion) versionText = status and versionText or "?" title:SetText("TSM Error Window ("..versionText..")") local hLine = frame:CreateTexture(nil, "ARTWORK") hLine:SetHeight(2) hLine:SetColorTexture(0.3, 0.3, 0.3, 1) hLine:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -10) hLine:SetPoint("TOPRIGHT", title, "BOTTOMRIGHT", 0, -10) local text = frame:CreateFontString() frame.text = text text:SetHeight(45) text:SetPoint("TOPLEFT", hLine, "BOTTOMLEFT", 8, -8) text:SetPoint("TOPRIGHT", hLine, "BOTTOMRIGHT", -8, -8) text:SetFontObject(GameFontNormal) text:SetTextColor(1, 1, 1, 1) text:SetJustifyH("LEFT") text:SetJustifyV("MIDDLE") local switchBtn = CreateFrame("Button", nil, frame) frame.switchBtn = switchBtn switchBtn:SetPoint("TOPRIGHT", -4, -10) switchBtn:SetWidth(100) switchBtn:SetHeight(20) local fontString = switchBtn:CreateFontString() fontString:SetFontObject(GameFontNormalSmall) fontString:SetJustifyH("CENTER") fontString:SetJustifyV("MIDDLE") switchBtn:SetFontString(fontString) switchBtn:SetScript("OnClick", function(self) private.errorFrame.showingError = not private.errorFrame.showingError if private.errorFrame.showingError then private.errorFrame.details = private.errorFrame.editBox:GetText() self:SetText("Hide Error") private.errorFrame.editBox:SetText(private.errorFrame.errorStr) else self:SetText("Show Error") private.errorFrame.editBox:SetText(private.errorFrame.details) end end) local hLine2 = frame:CreateTexture(nil, "ARTWORK") hLine2:SetHeight(2) hLine2:SetColorTexture(0.3, 0.3, 0.3, 1) hLine2:SetPoint("TOPLEFT", text, "BOTTOMLEFT", -8, -4) hLine2:SetPoint("TOPRIGHT", text, "BOTTOMRIGHT", 8, -4) local scrollFrame = CreateFrame("ScrollFrame", nil, frame, "UIPanelScrollFrameTemplate") scrollFrame:SetPoint("TOPLEFT", hLine2, "BOTTOMLEFT", 8, -4) scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 38) local editBox = CreateFrame("EditBox", nil, scrollFrame) frame.editBox = editBox editBox:SetWidth(scrollFrame:GetWidth()) editBox:SetFontObject(ChatFontNormal) editBox:SetMultiLine(true) editBox:SetAutoFocus(false) editBox:SetMaxLetters(0) editBox:SetTextColor(1, 1, 1, 1) editBox:SetScript("OnUpdate", function(self) local offset = scrollFrame:GetVerticalScroll() self:SetHitRectInsets(0, 0, offset, self:GetHeight() - offset - scrollFrame:GetHeight()) end) editBox:SetScript("OnEditFocusGained", function(self) self:HighlightText() end) editBox:SetScript("OnCursorChanged", function(self) if private.errorFrame.showingError and self:HasFocus() then self:HighlightText() end end) editBox:SetScript("OnEscapePressed", function(self) if private.errorFrame.showingError then self:HighlightText(0, 0) end self:ClearFocus() end) scrollFrame:SetScrollChild(editBox) local hLine3 = frame:CreateTexture(nil, "ARTWORK") hLine3:SetHeight(2) hLine3:SetColorTexture(0.3, 0.3, 0.3, 1) hLine3:SetPoint("BOTTOMLEFT", frame, 0, 35) hLine3:SetPoint("BOTTOMRIGHT", frame, 0, 35) local reloadBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") frame.reloadBtn = reloadBtn reloadBtn:SetPoint("BOTTOMLEFT", 4, 4) reloadBtn:SetWidth(120) reloadBtn:SetHeight(30) reloadBtn:SetText(RELOADUI) reloadBtn:SetScript("OnClick", function() frame:Hide() ReloadUI() end) local closeBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") frame.closeBtn = closeBtn closeBtn:SetPoint("BOTTOMRIGHT", -4, 4) closeBtn:SetWidth(120) closeBtn:SetHeight(30) closeBtn:SetText(DONE) closeBtn:SetScript("OnClick", function() frame:Hide() end) local stepsText = frame:CreateFontString() frame.stepsText = stepsText stepsText:SetWidth(200) stepsText:SetHeight(30) stepsText:SetPoint("BOTTOM", 0, 4) stepsText:SetFontObject(GameFontNormal) stepsText:SetTextColor(1, 0, 0, 1) stepsText:SetJustifyH("CENTER") stepsText:SetJustifyV("MIDDLE") stepsText:SetText("Please enter steps before submitting") end -- ============================================================================ -- Register Error Handler -- ============================================================================ do private.origErrorHandler = geterrorhandler() local function ErrorHandlerFunc(errMsg) local tsmErrMsg = strtrim(tostring(errMsg)) if private.ignoreErrors then -- we're ignoring errors tsmErrMsg = nil elseif strmatch(tsmErrMsg, "auc%-stat%-wowuction") or strmatch(tsmErrMsg, "TheUndermineJournal%.lua") or strmatch(tsmErrMsg, "[\\/]SavedVariables[\\/]TradeSkillMaster") or strmatch(tsmErrMsg, "AddOn TradeSkillMaster[_a-zA-Z]* attempted") or (strmatch(tsmErrMsg, "ItemTooltipClasses[\\/]Wrapper%.lua:98") and strmatch(tsmErrMsg, "SetQuest")) then -- explicitly ignore these errors tsmErrMsg = nil elseif strmatch(tsmErrMsg, "Blizzard_AuctionUI%.lua:751") then -- suppress this Blizzard error return true end if tsmErrMsg then -- look at the stack trace to see if this is a TSM error for i = 2, MAX_STACK_DEPTH do local stackLine = debugstack(i, 1, 0) if not strmatch(stackLine, "^%[C%]:") and not strmatch(stackLine, "%(tail call%):") and not strmatch(stackLine, "^%[string \"[^@]") and not strmatch(stackLine, "lMaster[\\/]External[\\/][A-Za-z0-9%-_%.]+[\\/]") and not strmatch(stackLine, "SharedXML") and not strmatch(stackLine, "CallbackHandler") and not strmatch(stackLine, "!BugGrabber") and not strmatch(stackLine, "ErrorHandler%.lua") then if not private.IsTSMAddon(stackLine) then tsmErrMsg = nil end break end end end if tsmErrMsg then local status, ret = pcall(private.ErrorHandler, tsmErrMsg) if status and ret then return ret elseif not status and not private.hitInternalError then private.hitInternalError = true print("Internal TSM error: "..tostring(ret)) end end return false end -- Wrap the error handler logic in a loadstring to avoid our locals / stack frames showing in other error handlers local WRAPPER_FUNC_STR = [[ return select(1, ...)(select(3, ...)) or (select(2, ...) and select(2, ...)((select(3, ...)))) or nil ]] local wrapperFunc = assert(loadstring(WRAPPER_FUNC_STR, "=[tsm error check]")) seterrorhandler(function(msg) return wrapperFunc(ErrorHandlerFunc, private.origErrorHandler, msg) end) if BugGrabber and BugGrabber.RegisterCallback then BugGrabber.RegisterCallback({}, "BugGrabber_BugGrabbed", function(_, errObj) ErrorHandlerFunc(errObj.message) end) end Event.Register("ADDON_ACTION_FORBIDDEN", private.AddonBlockedHandler) Event.Register("ADDON_ACTION_BLOCKED", private.AddonBlockedHandler) end