local _G = _G
local type , table , next , tostring , tonumber , print = type , table , next , tostring , tonumber , print
local debuglocals , debugstack , wipe , IsEncounterInProgress , GetTime = debuglocals , debugstack , table.wipe , IsEncounterInProgress , GetTime
local GetAddOnMetadata = C_AddOns.GetAddOnMetadata
local DisableAddOn = C_AddOns.DisableAddOn or DisableAddOn
local GetAddOnInfo = C_AddOns.GetAddOnInfo or GetAddOnInfo
local IsAddOnLoaded = C_AddOns.IsAddOnLoaded or IsAddOnLoaded
local GetNumAddOns = C_AddOns.GetNumAddOns or GetNumAddOns
-----------------------------------------------------------------------
-- Check if we already exist in the global space
-- If we do - bail out early, there's no version checks.
if _G.BugGrabber then return end
-----------------------------------------------------------------------
-- If we're embedded we create a .BugGrabber object on the addons
-- table, unless we find a standalone !BugGrabber addon.
local bugGrabberParentAddon , parentAddonTable = ...
local STANDALONE_NAME = " !BugGrabber "
if bugGrabberParentAddon ~= STANDALONE_NAME then
local tbl = { STANDALONE_NAME , " !Swatter " , " !ImprovedErrorFrame " }
for i = 1 , 3 do
local _ , _ , _ , enabled = GetAddOnInfo ( tbl [ i ] )
if enabled then return end -- Bail out
end
end
if not parentAddonTable.BugGrabber then parentAddonTable.BugGrabber = { } end
local addon = parentAddonTable.BugGrabber
local real_seterrorhandler = seterrorhandler
-----------------------------------------------------------------------
-- Global config variables
--
MAX_BUGGRABBER_ERRORS = 1000
-- If we get more errors than this per second, we stop all capturing
BUGGRABBER_ERRORS_PER_SEC_BEFORE_THROTTLE = 10
-----------------------------------------------------------------------
-- Localization
--
local L = {
ADDON_CALL_PROTECTED = " [%s] AddOn '%s' tried to call the protected function '%s'. " ,
ADDON_CALL_PROTECTED_MATCH = " ^%[(.*)%] (AddOn '.*' tried to call the protected function '.*'.)$ " ,
ADDON_DISABLED = " |cffffff00!BugGrabber and %s cannot coexist; %s has been forcefully disabled. If you want to, you may log out, disable !BugGrabber, and enable %s.|r " ,
BUGGRABBER_STOPPED = " |cffffff00There are too many errors in your UI. As a result, your game experience may be degraded. Disable or update the failing addons if you don't want to see this message again.|r " ,
ERROR_DETECTED = " %s |cffffff00captured, click the link for more information.|r " ,
ERROR_UNABLE = " |cffffff00!BugGrabber is unable to retrieve errors from other players by itself. Please install BugSack or a similar display addon that might give you this functionality.|r " ,
NO_DISPLAY_1 = " |cffffff00You seem to be running !BugGrabber with no display addon to go along with it. Although a slash command is provided for accessing error reports, a display can help you manage these errors in a more convenient way.|r " ,
NO_DISPLAY_2 = " |cffffff00The standard display is called BugSack, and can probably be found on the same site where you found !BugGrabber.|r " ,
NO_DISPLAY_STOP = " |cffffff00If you don't want to be reminded about this again, run /stopnag.|r " ,
STOP_NAG = " |cffffff00!BugGrabber will not nag about missing a display addon again until next patch.|r " ,
USAGE = " |cffffff00Usage: /buggrabber <1-%d>.|r " ,
}
-----------------------------------------------------------------------
-- Locals
--
-- Should implement :FormatError(errorTable).
local displayObjectName = nil
for i = 1 , GetNumAddOns ( ) do
local meta = GetAddOnMetadata ( i , " X-BugGrabber-Display " )
if meta then
local _ , _ , _ , enabled = GetAddOnInfo ( i )
if enabled then
displayObjectName = meta
break
end
end
end
-- Shorthand to BugGrabberDB.errors
local db = nil
-- Errors we catch during the addon loading process, before our saved
-- variables are available. After the SVs have loaded, these will be
-- inserted into the proper DB.
local loadErrors = { }
local paused = nil
local isBugGrabbedRegistered = nil
local callbacks = nil
local playerName = UnitName ( " player " )
local chatLinkFormat = " |Hbuggrabber:%s:%s:|h|cffff0000[Error %s]|r|h "
local tableToString = " table: %s "
-----------------------------------------------------------------------
-- Callbacks
--
local function setupCallbacks ( )
if not callbacks and LibStub and LibStub ( " CallbackHandler-1.0 " , true ) then
callbacks = LibStub ( " CallbackHandler-1.0 " ) : New ( addon )
function callbacks : OnUsed ( _ , eventname )
if eventname == " BugGrabber_BugGrabbed " then isBugGrabbedRegistered = true end
end
function callbacks : OnUnused ( _ , eventname )
if eventname == " BugGrabber_BugGrabbed " then isBugGrabbedRegistered = nil end
end
setupCallbacks = nil
end
end
addon.setupCallbacks = setupCallbacks ; -- make it accessible from the outside for add-ons relying on BugGrabber events so they can make BugGrabber.RegisterCallback appear when they need it (CallbackHandler-1.0 is not embedded in BugGrabber)
local function triggerEvent ( ... )
if not callbacks then setupCallbacks ( ) end
if callbacks then callbacks : Fire ( ... ) end
end
-----------------------------------------------------------------------
-- Utility
--
local function fetchFromDatabase ( database , target )
for i , err in next , database do
if err.message == target then
-- This error already exists
err.counter = err.counter + 1
err.session = addon : GetSessionId ( )
return table.remove ( database , i )
end
end
end
local function printErrorObject ( err )
local found = nil
if displayObjectName and _G [ displayObjectName ] then
local display = _G [ displayObjectName ]
if type ( display ) == " table " and type ( display.FormatError ) == " function " then
found = true
print ( display : FormatError ( err ) )
end
end
if not found then
print ( err.message )
if err.stack then
print ( err.stack )
end
if err.locals then
print ( err.locals )
end
end
end
-----------------------------------------------------------------------
-- Slash handler
--
local function slashHandler ( index )
if not db then return end
index = tonumber ( index )
local err = type ( index ) == " number " and db [ index ] or nil
if not index or not err or type ( err ) ~= " table " or ( type ( err.message ) ~= " string " and type ( err.message ) ~= " table " ) then
print ( L.USAGE : format ( # db ) )
return
end
printErrorObject ( err )
end
-----------------------------------------------------------------------
-- Error catching
--
local findVersions -- Function set below
do
local function scanObject ( o )
local version , revision = nil , nil
for k , v in next , o do
if type ( k ) == " string " and ( type ( v ) == " string " or type ( v ) == " number " ) then
local low = k : lower ( )
if not version and low : find ( " version " ) then
version = v
elseif not revision and low : find ( " revision " ) then
revision = v
end
end
if version and revision then break end
end
return version , revision
end
local matchCache = setmetatable ( { } , { __index = function ( self , object )
if type ( object ) ~= " string " or # object < 3 then return end
local found = nil
-- First see if it's a library
if LibStub then
local _ , minor = LibStub ( object , true )
found = minor
end
-- Then see if we can get some addon metadata
if not found and IsAddOnLoaded ( object ) then
found = GetAddOnMetadata ( object , " X-Curse-Packaged-Version " )
if not found then
found = GetAddOnMetadata ( object , " Version " )
end
end
-- Perhaps it's a global object?
if not found then
local o = _G [ object ] or _G [ object : upper ( ) ]
if type ( o ) == " table " then
local v , r = scanObject ( o )
if v or r then
found = tostring ( v ) .. " . " .. tostring ( r )
end
elseif o then
found = o
end
end
if not found then
found = _G [ object : upper ( ) .. " _VERSION " ]
end
if type ( found ) == " string " or type ( found ) == " number " then
self [ object ] = found
return found
end
end } )
local tmp = { }
local function replacer ( start , object , tail )
-- Have we matched this object before on the same line?
-- (another pattern could re-match a previous match...)
if tmp [ object ] then return end
local found = matchCache [ object ]
if found then
tmp [ object ] = true
return ( type ( start ) == " string " and start or " " ) .. object .. " - " .. found .. ( type ( tail ) == " string " and tail or " " )
end
end
local matchers = {
" ( \\ )([^ \\ ]+)(%.lua) " , -- \Anything-except-backslashes.lua
" ^()([^ \\ ]+)( \\ ) " , -- Start-of-the-line-until-first-backslash\
" ()(%a+%-%d%.?%d?)() " , -- Anything-#.#, where .# is optional
" ()(Lib%u%a+%-?%d?%.?%d?)() " -- LibXanything-#.#, where X is any capital letter and -#.# is optional
}
function findVersions ( line )
if not line or line : find ( " FrameXML \\ " ) then return line end
for i = 1 , 4 do
line = line : gsub ( matchers [ i ] , replacer )
end
wipe ( tmp )
return line
end
end
-- Error handler
local grabError
do
local msgsAllowed = BUGGRABBER_ERRORS_PER_SEC_BEFORE_THROTTLE
local msgsAllowedLastTime = GetTime ( )
local lastWarningTime = 0
function grabError ( errorMessage , isSimple )
-- Flood protection --
msgsAllowed = msgsAllowed + ( GetTime ( ) - msgsAllowedLastTime ) * BUGGRABBER_ERRORS_PER_SEC_BEFORE_THROTTLE
msgsAllowedLastTime = GetTime ( )
if msgsAllowed < 1 then
if not paused then
if bugGrabberParentAddon == STANDALONE_NAME then
if GetTime ( ) > lastWarningTime + 10 then
print ( L.BUGGRABBER_STOPPED )
lastWarningTime = GetTime ( )
end
end
paused = true
triggerEvent ( " BugGrabber_CapturePaused " )
end
return
end
paused = false
if msgsAllowed > BUGGRABBER_ERRORS_PER_SEC_BEFORE_THROTTLE then
msgsAllowed = BUGGRABBER_ERRORS_PER_SEC_BEFORE_THROTTLE
end
msgsAllowed = msgsAllowed - 1
-- Grab it --
errorMessage = tostring ( errorMessage )
local looping = errorMessage : find ( " BugGrabber " ) and true or nil
if looping then
print ( errorMessage )
return
end
local sanitizedMessage = findVersions ( errorMessage )
-- Insert the error into the correct database if it's not there
-- already. If it is, just increment the counter.
local found
if db then
found = fetchFromDatabase ( db , sanitizedMessage )
else
found = fetchFromDatabase ( loadErrors , sanitizedMessage )
end
-- XXX Note that fetchFromDatabase will set the error objects
-- XXX session ID to the current one, if found - and it will also
-- XXX increment the counter on it. This is probably wrong, it should
-- XXX be done here instead, as "fetchFromDatabase" implies a simple
-- XXX :Get procedure.
local errorObject = found
if not errorObject then
-- Store the error
if isSimple then
errorObject = {
message = sanitizedMessage ,
session = addon : GetSessionId ( ) ,
time = date ( " %Y/%m/%d %H:%M:%S " ) ,
counter = 1 ,
}
else
local stack = debugstack ( 3 )
local tbl = { }
-- Scan for version numbers in the stack
if stack then
for line in stack : gmatch ( " (.-) \n " ) do
tbl [ # tbl + 1 ] = findVersions ( line )
end
end
local inCombat = IsEncounterInProgress ( ) -- debuglocals can be slow sometimes (200ms+)
errorObject = {
message = sanitizedMessage ,
stack = stack and table.concat ( tbl , " \n " ) or " Debugstack was nil. " ,
locals = inCombat and " Skipped (In Encounter) " or debuglocals ( 3 ) ,
session = addon : GetSessionId ( ) ,
time = date ( " %Y/%m/%d %H:%M:%S " ) ,
counter = 1 ,
}
end
end
if not isBugGrabbedRegistered then
print ( L.ERROR_DETECTED : format ( addon : GetChatLink ( errorObject ) ) )
end
addon : StoreError ( errorObject )
triggerEvent ( " BugGrabber_BugGrabbed " , errorObject )
end
end
-----------------------------------------------------------------------
-- API
--
function addon : StoreError ( errorObject )
if db then
db [ # db + 1 ] = errorObject
-- Save only the last MAX_BUGGRABBER_ERRORS errors (otherwise the SV gets too big)
if # db > MAX_BUGGRABBER_ERRORS then
table.remove ( db , 1 )
end
else
loadErrors [ # loadErrors + 1 ] = errorObject
end
end
do
local function createChatHook ( )
-- Set up the ItemRef hook that allow us to link bugs.
local SetHyperlink = ItemRefTooltip.SetHyperlink
function ItemRefTooltip : SetHyperlink ( link , ... )
local player , tableId = link : match ( " ^buggrabber:([^:]+):([^:]+): " )
if player then
addon : HandleBugLink ( player , tableId , link )
else
SetHyperlink ( self , link , ... )
end
end
end
-- We need to hook the chat frame when anyone requests a chat link from us,
-- in case some other addon has hooked :HandleBugLink to process it. If not,
-- we could've just created the hook in grabError when we do the print.
function addon : GetChatLink ( errorObject )
if createChatHook then createChatHook ( ) createChatHook = nil end
local tableId = tostring ( errorObject ) : sub ( 8 )
return chatLinkFormat : format ( playerName , tableId , tableId )
end
end
function addon : GetErrorByPlayerAndID ( player , id )
if player == playerName then return addon : GetErrorByID ( id ) end
print ( L.ERROR_UNABLE )
end
function addon : GetErrorByID ( id )
local errorId = tableToString : format ( id )
for _ , err in next , db do
if tostring ( err ) == errorId then
return err
end
end
end
function addon : GetErrorID ( errorObject ) return tostring ( errorObject ) : sub ( 8 ) end
function addon : Reset ( ) if BugGrabberDB then wipe ( BugGrabberDB.errors ) end end
function addon : GetDB ( ) return db or loadErrors end
function addon : GetSessionId ( ) return BugGrabberDB and BugGrabberDB.session or - 1 end
function addon : IsPaused ( ) return paused end
function addon : HandleBugLink ( player , id )
local errorObject = addon : GetErrorByPlayerAndID ( player , id )
if errorObject then
printErrorObject ( errorObject )
end
end
-----------------------------------------------------------------------
-- Initialization
--
local function initDatabase ( )
-- Persist defaults and make sure we have sane SavedVariables
if type ( BugGrabberDB ) ~= " table " then BugGrabberDB = { } end
local sv = BugGrabberDB
if type ( sv.session ) ~= " number " then sv.session = 0 end
if type ( sv.errors ) ~= " table " then sv.errors = { } end
-- From now on we can persist errors. Create a new session.
sv.session = sv.session + 1
-- Determine the correct database
db = BugGrabberDB.errors -- db is a file-local variable
-- Cut down on the nr of errors if it is over the MAX_BUGGRABBER_ERRORS
while # db > MAX_BUGGRABBER_ERRORS do
table.remove ( db , 1 )
end
-- If there were any load errors, we need to iterate them and
-- insert the relevant ones into our SV DB.
for _ , err in next , loadErrors do
err.session = sv.session -- Update the session ID directly
local exists = fetchFromDatabase ( db , err.message )
addon : StoreError ( exists or err )
end
wipe ( loadErrors )
if type ( sv.lastSanitation ) ~= " number " or sv.lastSanitation ~= 3 then
for i , v in next , db do
if type ( v.message ) == " table " then table.remove ( db , i ) end
end
sv.lastSanitation = 3
end
-- load locales
if type ( addon.LoadTranslations ) == " function " then
local locale = GetLocale ( )
if locale ~= " enUS " and locale ~= " enGB " then
addon : LoadTranslations ( locale , L )
end
addon.LoadTranslations = nil
end
-- Only warn about missing display if we're running standalone.
if not displayObjectName and bugGrabberParentAddon == STANDALONE_NAME then
local _ , _ , _ , currentInterface = GetBuildInfo ( )
if type ( currentInterface ) ~= " number " then currentInterface = 0 end
if not sv.stopnag or sv.stopnag < currentInterface then
print ( L.NO_DISPLAY_1 )
print ( L.NO_DISPLAY_2 )
print ( L.NO_DISPLAY_STOP )
_G.SlashCmdList . BugGrabberStopNag = function ( )
print ( L.STOP_NAG )
sv.stopnag = currentInterface
end
_G.SLASH_BugGrabberStopNag1 = " /stopnag "
end
end
initDatabase = nil
end
local events = { }
do
local frame = CreateFrame ( " Frame " )
frame : SetScript ( " OnEvent " , function ( _ , event , ... ) events [ event ] ( events , event , ... ) end )
frame : RegisterEvent ( " ADDON_LOADED " )
frame : RegisterEvent ( " PLAYER_LOGIN " )
frame : RegisterEvent ( " ADDON_ACTION_BLOCKED " )
frame : RegisterEvent ( " ADDON_ACTION_FORBIDDEN " )
frame : RegisterEvent ( " LUA_WARNING " )
local function noop ( ) end -- Prevent abusive addons
frame.RegisterEvent = noop
frame.UnregisterEvent = noop
frame.SetScript = noop
end
do
local function createSwatter ( )
-- Need this so Stubby will feed us errors instead of just
-- dumping them to the chat frame.
_G.Swatter = {
IsEnabled = function ( ) return true end ,
OnError = function ( msg , _ , stack )
grabError ( tostring ( msg ) .. tostring ( stack ) )
end ,
isFake = true ,
}
end
local swatterDisabled = nil
function events : ADDON_LOADED ( _ , msg )
if not callbacks then setupCallbacks ( ) end
if msg == " Stubby " then createSwatter ( ) end
if initDatabase then
-- If we're running embedded, just init as soon as possible,
-- but if we are running separately we init when !BugGrabber
-- loads so that our SVs are available.
if bugGrabberParentAddon ~= STANDALONE_NAME or msg == bugGrabberParentAddon then
initDatabase ( )
end
end
if not swatterDisabled and _G.Swatter and not _G.Swatter . isFake then
swatterDisabled = true
if bugGrabberParentAddon == STANDALONE_NAME then
print ( L.ADDON_DISABLED : format ( " Swatter " , " Swatter " , " Swatter " ) )
end
DisableAddOn ( " !Swatter " )
SlashCmdList.SWATTER = nil
SLASH_SWATTER1 , SLASH_SWATTER2 = nil , nil
for _ , v in next , Swatter do
if type ( v ) == " table " then
if v.UnregisterAllEvents then
v : UnregisterAllEvents ( )
end
if v.Hide then
v : Hide ( )
end
end
end
Swatter = nil
local _ , _ , _ , enabled = GetAddOnInfo ( " Stubby " )
if enabled then createSwatter ( ) end
real_seterrorhandler ( grabError )
end
end
end
function events : PLAYER_LOGIN ( )
if not callbacks then setupCallbacks ( ) end
real_seterrorhandler ( grabError )
end
do
local badAddons = { }
function events : ADDON_ACTION_FORBIDDEN ( event , addonName , addonFunc )
local name = addonName or " <name> "
if not badAddons [ name ] then
badAddons [ name ] = true
grabError ( L.ADDON_CALL_PROTECTED : format ( event , name or " <name> " , addonFunc or " <func> " ) )
end
end
end
events.ADDON_ACTION_BLOCKED = events.ADDON_ACTION_FORBIDDEN
function events : LUA_WARNING ( _ , warnType , warningText )
-- Temporary hack for the few dropdown libraries that exist that were designed poorly
-- Hopefully we will see a rewrite of dropdowns soon
if warnType == 0 and warningText : find ( " DropDown " , nil , true ) then return end
grabError ( warningText , true )
end
UIParent : UnregisterEvent ( " LUA_WARNING " )
real_seterrorhandler ( grabError )
function seterrorhandler ( ) --[[ noop ]] end
-- Set up slash command
SlashCmdList.BugGrabber = slashHandler
SLASH_BugGrabber1 = " /buggrabber "
BugGrabber = setmetatable ( { } , { __index = addon , __newindex = function ( ) end , __metatable = false } )