770 lines
26 KiB
Lua
770 lines
26 KiB
Lua
|
-- ------------------------------------------------------------------------------ --
|
||
|
-- TradeSkillMaster --
|
||
|
-- https://tradeskillmaster.com --
|
||
|
-- All Rights Reserved - Detailed license information included with addon. --
|
||
|
-- ------------------------------------------------------------------------------ --
|
||
|
|
||
|
-- TSM's error handler
|
||
|
|
||
|
local _, 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 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 OLD_TSM_MODULES = {
|
||
|
"TradeSkillMaster_Accounting",
|
||
|
"TradeSkillMaster_AuctionDB",
|
||
|
"TradeSkillMaster_Auctioning",
|
||
|
"TradeSkillMaster_Crafting",
|
||
|
"TradeSkillMaster_Destroying",
|
||
|
"TradeSkillMaster_Mailing",
|
||
|
"TradeSkillMaster_Shopping",
|
||
|
"TradeSkillMaster_Vendoring",
|
||
|
"TradeSkillMaster_Warehousing",
|
||
|
}
|
||
|
local PRINT_PREFIX = "|cffff0000TSM:|r "
|
||
|
|
||
|
|
||
|
|
||
|
-- ============================================================================
|
||
|
-- Module Functions
|
||
|
-- ============================================================================
|
||
|
|
||
|
function ErrorHandler.ShowForThread(err, thread)
|
||
|
local stackLine = debugstack(thread, 0, 1, 0)
|
||
|
local oldModule = strmatch(stackLine, "(lMaster_[A-Za-z]+)")
|
||
|
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||
|
-- ignore errors from old modules
|
||
|
return
|
||
|
end
|
||
|
-- 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 = private.GetStackInfo(msg, thread)
|
||
|
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.locals ~= "" and ("\n |cffaaaaaa"..gsub(info.locals, "\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.__IS_TEST_ENV 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 stackInfo = {}
|
||
|
local stackStarted = false
|
||
|
for i = 0, MAX_STACK_DEPTH do
|
||
|
local prevStackFunc = #stackInfo > 0 and stackInfo[#stackInfo].func or nil
|
||
|
local file, line, func, localsStr, newPrevStackFunc = private.GetStackLevelInfo(i, thread, prevStackFunc)
|
||
|
if newPrevStackFunc then
|
||
|
stackInfo[#stackInfo].func = newPrevStackFunc
|
||
|
end
|
||
|
if file then
|
||
|
if not stackStarted then
|
||
|
if errLocation then
|
||
|
stackStarted = strmatch(file..":"..line, "[A-Za-z]+%.lua:[0-9]+") == errLocation
|
||
|
else
|
||
|
stackStarted = i > (thread and 1 or 4) and file ~= "[C]"
|
||
|
end
|
||
|
end
|
||
|
if stackStarted then
|
||
|
tinsert(stackInfo, {
|
||
|
file = file,
|
||
|
line = line,
|
||
|
func = func,
|
||
|
locals = localsStr,
|
||
|
})
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
return stackInfo
|
||
|
end
|
||
|
|
||
|
function private.GetStackLevelInfo(level, thread, prevStackFunc)
|
||
|
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 "?"
|
||
|
|
||
|
if strfind(locationStr, "LibTSMClass%.lua:") then
|
||
|
-- ignore stack frames from the class code's wrapper function
|
||
|
if func ~= "?" and prevStackFunc and not strmatch(func, "^.+:[0-9]+$") and strmatch(prevStackFunc, "^.+:[0-9]+$") then
|
||
|
-- this stack frame includes the class method we were accessing in the previous one, so go back and fix it up
|
||
|
local className = locals and strmatch(locals, "\n +str = \"([A-Za-z_0-9]+):[0-9A-F]+\"\n") or "?"
|
||
|
prevStackFunc = className.."."..func
|
||
|
end
|
||
|
return nil, nil, nil, nil, prevStackFunc
|
||
|
end
|
||
|
|
||
|
-- add locals for addon functions (debuglocals() doesn't always work - or ever for threads)
|
||
|
local localsStr = locals and private.ParseLocals(locals, file) or ""
|
||
|
return file, line, func, localsStr, nil
|
||
|
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 = false, false, false, false
|
||
|
wipe(private.localLinesTemp)
|
||
|
locals = gsub(locals, "<([a-z]+)> {[\n\t ]+}", "<%1> {}")
|
||
|
locals = gsub(locals, " = <function> defined @", "@")
|
||
|
locals = gsub(locals, "<table> {", "{")
|
||
|
|
||
|
for localLine in gmatch(locals, "[^\n]+") do
|
||
|
local shouldIgnoreLine = false
|
||
|
if strmatch(localLine, "^ *%(") then
|
||
|
shouldIgnoreLine = true
|
||
|
elseif strmatch(localLine, "LibTSMClass%.lua:") then
|
||
|
-- ignore class methods
|
||
|
shouldIgnoreLine = true
|
||
|
elseif strmatch(localLine, "<unnamed> {}$") then
|
||
|
-- ignore internal WoW frame members
|
||
|
shouldIgnoreLine = true
|
||
|
end
|
||
|
if not shouldIgnoreLine then
|
||
|
local 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 isLocaleTable then
|
||
|
-- ignore everything within the locale table
|
||
|
shouldIgnoreLine = true
|
||
|
elseif isPackageTable then
|
||
|
-- ignore the package table 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 = <userdata>$") 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
|
||
|
end
|
||
|
end
|
||
|
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
|
||
|
else
|
||
|
i = i - 1
|
||
|
end
|
||
|
end
|
||
|
return #private.localLinesTemp > 0 and table.concat(private.localLinesTemp, "\n") 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 "<name>", addonFunc or "<func>")
|
||
|
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, TSM.IsShadowlands() 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, isBugGrabber)
|
||
|
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
|
||
|
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)
|
||
|
local oldModule = strmatch(stackLine, "(lMaster_[A-Za-z]+)")
|
||
|
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||
|
-- ignore errors from old modules
|
||
|
return
|
||
|
end
|
||
|
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
|
||
|
local oldModule = strmatch(errMsg, "(lMaster_[A-Za-z]+)")
|
||
|
if oldModule and tContains(OLD_TSM_MODULES, "TradeSkil"..oldModule) then
|
||
|
-- ignore errors from old modules
|
||
|
return
|
||
|
end
|
||
|
if not isBugGrabber then
|
||
|
return private.origErrorHandler and private.origErrorHandler(errMsg) or nil
|
||
|
end
|
||
|
end
|
||
|
seterrorhandler(ErrorHandlerFunc)
|
||
|
if BugGrabber and BugGrabber.RegisterCallback then
|
||
|
BugGrabber.RegisterCallback({}, "BugGrabber_BugGrabbed", function(_, errObj)
|
||
|
ErrorHandlerFunc(errObj.message, true)
|
||
|
end)
|
||
|
end
|
||
|
Event.Register("ADDON_ACTION_FORBIDDEN", private.AddonBlockedHandler)
|
||
|
Event.Register("ADDON_ACTION_BLOCKED", private.AddonBlockedHandler)
|
||
|
end
|