--[[ Swatter - An AddOn debugging aid for World of Warcraft. Version: 5.9.4961 (WhackyWallaby) Revision: $Id: Swatter.lua 274 2010-09-24 21:57:26Z kandoko $ URL: http://auctioneeraddon.com/dl/Swatter/ Copyright (C) 2006 Norganna This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA ]] -- Check to see if another debugging aid has been loaded. for addon, name in pairs({ ['!buggrabber'] = 'BugGrabber', ['!improvederrorframe'] = 'ImprovedErrorFrame', }) do local enabled = select(4, GetAddOnInfo(addon)) if enabled then DEFAULT_CHAT_FRAME:AddMessage("|cffffaa11Swatter is not loaded, because you are running "..name.."|r") return end end Swatter = { origHandler = geterrorhandler(), origItemRef = SetItemRef, nilFrame = { GetName = function() return "Global" end }, errorOrder = {}, moduleDetail = {}, HISTORY_SIZE = 100, } local origItemRef = Swatter.origItemRef Swatter.Version="5.9.4961" if (Swatter.Version == "<%".."version%>") then Swatter.Version = "4.1.DEV" end SWATTER_VERSION = Swatter.Version SwatterData = { enabled = true, autoshow = true, errors = {}, } local tinsert = table.insert -- SetAddOnDetail global function: -- (See http://wowwiki.com/SetAddOnDetail for details) -- Define a blank stub if the function doesn't exist. if not _G['SetAddOnDetail'] then _G['SetAddOnDetail'] = function(name, detail) end end -- Define my functions to process the information. local function addOnDetail(name, detail) if not detail.name then detail.name = name end Swatter.moduleDetail[name:lower()] = detail end -- Post hook the global SetAddOnDetail function. hooksecurefunc("SetAddOnDetail", addOnDetail) -- End SetAddOnDetail function hook. LibStub("LibRevision"):Set("$URL: http://svn.norganna.org/libs/trunk/!Swatter/Swatter.lua $","$Rev: 274 $","5.1.DEV.", 'auctioneer', 'libs') local function toggle() if Swatter.Error:IsVisible() then Swatter.Error:Hide() else Swatter.ErrorShow() end end local function addSlideIcon() if LibStub then local LibDataBroker = LibStub:GetLibrary("LibDataBroker-1.1", true) if not LibDataBroker then return end local LDBButton = LibDataBroker:NewDataObject("Swatter", { type = "launcher", icon = "Interface\\AddOns\\!Swatter\\Textures\\SwatterIcon", OnClick = function(self, button) toggle(self, button) end, }) function LDBButton:OnTooltipShow() self:AddLine("Swatter", 1,1,0.5, 1) self:AddLine("Swatter is a bug catcher that performs additional backtracing to allow AddOn authors to easily trace errors when you send them error reports. You may disable this AddOn if you never get bugs, don't care about them, or never report them when you do get them.", 1,1,0.5, 1) self:AddLine("|cff1fb3ff".."Click".."|r ".."to open the report.", 1,1,0.5, 1) end function LDBButton:OnEnter() GameTooltip:SetOwner(self, "ANCHOR_NONE") GameTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT") GameTooltip:ClearLines() LDBButton.OnTooltipShow(GameTooltip) GameTooltip:Show() end function LDBButton:OnLeave() GameTooltip:Hide() end end end function Swatter.ChatMsg(msg) DEFAULT_CHAT_FRAME:AddMessage(msg) end local chat = Swatter.ChatMsg function Swatter.IsEnabled() return SwatterData.enabled end -- Test: /run Swatter.OnError("Test") function Swatter.OnError(msg, frame, stack, etype, ...) if (not SwatterData.enabled) then if (not etype) then return Swatter.origHandler(msg, frame) else return UIParent_OnEvent(etype, ...) end end msg = msg or "" frame = frame or Swatter.nilFrame stack = stack or debugstack(2, 20, 20) local context if (not frame.Swatter) then frame.Swatter = {} end local id = frame.Swatter[msg] -- id might still exist if we've done a clear, because we don't cycle through the frames killing frame.Swatter data -- So we have to check for both here if (not ( id and #(SwatterData.errors) ~= 0)) then context = "Anonymous" if (frame) then context = "Unnamed" if (frame:GetName()) then context = frame:GetName() end end local timestamp = date("%Y-%m-%d %H:%M:%S"); local addons = Swatter.GetAddOns() tinsert(SwatterData.errors, { context = context, timestamp = timestamp, addons = addons, message = msg, stack = stack, count = 0, }) id = table.getn(SwatterData.errors) frame.Swatter[msg] = id else context = SwatterData.errors[id].context for pos, errid in ipairs(Swatter.errorOrder) do if (errid == id) then table.remove(Swatter.errorOrder, pos) break end end end tinsert(Swatter.errorOrder, id) local err = SwatterData.errors[id] local count = err.count or 0 if (count < 1000) then err.count = count + 1 end if (count == 0) then if (etype == "ADDON_ACTION_BLOCKED") then if (not Swatter.blockWarn) then chat("|cffffaa11Warning only: Swatter found blocked actions:|r |Hswatter:"..id.."|h|cffff3311["..context.."]|r|h") chat("|cffffaa11Note: Swatter will continue to catch blocked actions but this is the last time this session that we'll tell you about it.|r") Swatter.blockWarn = true end elseif (SwatterData.autoshow) then Swatter.ErrorUpdate() Swatter.Error:Show() else chat("|cffffaa11Swatter caught error:|r |Hswatter:"..id.."|h|cffff3311["..context.."]|r|h") end end end function Swatter.NamedFrame(name) if (not Swatter.named) then Swatter.named = {} end if (not Swatter.named[name]) then Swatter.named[name] = { name = name, GetName = function(obj) return obj.name end, } end return Swatter.named[name] end local function keyPairs(t,f) local a, i = {}, 0 for n in pairs(t) do table.insert(a, n) end table.sort(a, f) local iter = function () i = i + 1 if a[i] == nil then return nil else return a[i], t[a[i]] end end return iter end function Swatter.GetConfig() local locale = GetLocale() local realmList = GetCVar("realmList") or "none" local version, build, date, tocversion = GetBuildInfo() local trimmedRealmList = realmList:gsub("\.logon\.worldofwarcraft\.com", "") local configString = string.format(" BlizRuntimeLib_%s v%s.%s \<%s\>\n", locale, version, tocversion, trimmedRealmList ) return configString end function Swatter.GetAddOns() local addlist = "" local addons = {} for i = 1, GetNumAddOns() do local name, title, notes, enabled, loadable, reason, security = GetAddOnInfo(i) local loaded = IsAddOnLoaded(i) if (loaded) then local version = GetAddOnMetadata(i, "Version") local addition = GetAddOnMetadata(i, "X-Swatter-Extra") if not version then version = GetAddOnMetadata(i, "X-Version") end if not version then version = "" end addons[name:lower()] = { name = name, version = version, ["x-swatter-extra"] = addition, } end end for addon, reg in pairs(Swatter.moduleDetail) do addons[addon] = reg end for addon, reg in keyPairs(addons) do local version local addition = "" local name = reg.name version = reg.version if (reg["x-swatter-extra"]) then addition = "("..reg["x-swatter-extra"]..")" end name = name:gsub("[^a-zA-Z0-9]+", "") if not version then local class = _G[name] if not class or type(class)~='table' then class = _G[name:lower()] end if not class or type(class)~='table' then class = _G[name:sub(1,1):upper()..name:sub(2):lower()] end if not class or type(class)~='table' then class = _G[name:upper()] end if class and type(class)=='table' then if (class.version) then version = class.version elseif (class.Version) then version = class.Version elseif (class.VERSION) then version = class.VERSION end end end if not version then local const = _G[name:upper().."_VERSION"] if (const) then version = const end end if type(version)=='table' then if (nLog) then nLog.AddMessage("!swatter", "Swatter.lua", N_INFO, "version is a table", name, table.concat(version,":")) end version = table.concat(version,":") end if (version) then addlist = addlist.." "..name..", v"..tostring(version)..addition.."\n" else addlist = addlist.." "..name.."\n" end end addlist = addlist..Swatter.GetConfig() addlist = addlist..string.format(" (ck=%x)\n", addlist:len()) return addlist end -- Error occured in: Global -- Date: "%m/%d/%y %H:%M:%S" -- Count: 1 -- Message: [string "bla()"] line 1: -- attempt to call global 'bla' (a nil value) -- Debug: -- [C]: in function `bla' -- [string "bla()"]:1: in main chunk -- [C]: in function `RunScript' -- Interface\FrameXML\ChatFrame.lua:1788: in function `value' -- Interface\FrameXML\ChatFrame.lua:3008: in function `ChatEdit_ParseText' -- Interface\FrameXML\ChatFrame.lua:2734: in function `ChatEdit_SendText' -- Interface\FrameXML\ChatFrame.lua:2756: in function `ChatEdit_OnEnterPressed' -- [string "ChatFrameEditBox:OnEnterPressed"]:2: in function <[string "ChatFrameEditBox:OnEnterPressed"]:1> function Swatter.OnEvent(frame, event, ...) if (event == "ADDON_LOADED") then local addon = ... if (addon:lower() == "!swatter") then -- Check to see if we still exist if (not SwatterData) then if (BugGrabber) then -- We've been buggrabber-nabbed. Give up. DEFAULT_CHAT_FRAME:AddMessage("|cffffaa11Warning: Swatter has been disabled by BugGrabber. If you want to run Swatter instead of BugGrabber/BugSack, disable those two addons in you addon list and re-enable Swatter. Otherwise, enjoy BugGrabber!|r"); end SetItemRef = origItemRef return end -- We need to cleanup our error history if (not SwatterData.errors) then SwatterData.errors = {} end local ec = table.getn(SwatterData.errors) or 0 if (ec > Swatter.HISTORY_SIZE) then local remove = ec - Swatter.HISTORY_SIZE for i=1, remove do table.remove(SwatterData.errors, 1) end end for pos, err in ipairs(SwatterData.errors) do tinsert(Swatter.errorOrder, pos) end Swatter.loadCount = #SwatterData.errors Swatter.lastShown = Swatter.loadCount return end elseif (event == "ADDON_ACTION_BLOCKED" and SwatterData.warning) then local addon, func = ... if (InCombatLockdown()) then Swatter.OnError(string.format("Note: AddOn %s attempted to call a protected function (%s) during combat lockdown.", addon, func), Swatter.NamedFrame("AddOn: "..addon), debugstack(2, 20, 20), event, ...) else Swatter.OnError(string.format("Warning: AddOn %s attempted to call a protected function (%s) which may require interaction.", addon, func), Swatter.NamedFrame("AddOn: "..addon), debugstack(2, 20, 20), event, ...) end elseif (event == "ADDON_ACTION_FORBIDDEN") then local addon, func = ... Swatter.OnError(string.format("Error: AddOn %s attempted to call a forbidden function (%s) from a tainted execution path.", addon, func), Swatter.NamedFrame("AddOn: "..addon), debugstack(2, 20, 20), event, ...) elseif (event == "PLAYER_LOGIN") then addSlideIcon() --create ldb launcher button -- Check to see if any events have happened since we loaded and the player is ready to view them local curCount = #SwatterData.errors local loadCount = Swatter.loadCount if curCount > loadCount then Swatter.ErrorShow() end elseif (event == "PLAYER_ENTERING_WORLD") then tinsert(UISpecialFrames, "SwatterProxyFrame") Swatter.Frame:UnregisterEvent("PLAYER_ENTERING_WORLD") end end function Swatter.SetItemRef(...) local msg = ... local id = select(3, msg:find("^swatter:(%d+)")) id = tonumber(id) if (id) then if (Swatter) then for pos, errid in ipairs(Swatter.errorOrder) do if (errid == id) then Swatter.ErrorShow(pos) return end end end else if (not Swatter) then SetItemRef = origItemRef return origItemRef(...) end return Swatter.origItemRef(...) end end function Swatter.ErrorShow(pos) local maxError = #SwatterData.errors local curError = tonumber(pos) if not curError then curError = Swatter.lastShown if curError then curError = min(curError + 1, maxError) else curError = maxError end end Swatter.Error.pos = curError or -1 if (Swatter.Error.pos == 0) then Swatter.Error.pos = -1 end Swatter.ErrorDisplay() end function Swatter.ErrorDisplay(id) if id then Swatter.Error.pos = id else id = Swatter.Error.pos end Swatter.ErrorUpdate() if (id == -1) then Swatter.Error.curError = "There are no errors to show." Swatter.ErrorUpdate() return end local errid = Swatter.errorOrder[id] if (not errid) then Swatter.Error.curError = "Unknown error at position "..id Swatter.ErrorUpdate() return end local err = SwatterData.errors[errid] if (not err) then Swatter.Error.curError = "Unknown error at index "..errid Swatter.ErrorUpdate() return end local timestamp = err.timestamp or "Unavailable" local addlist = err.addons or " Unavailable" local context = err.context or "Anonymous" local message = err.message:gsub("(.-):(%d+): ", "%1 line %2:\n "):gsub("Interface(\\%w+\\)", "..%1"):gsub(": in function `(.-)`", ": %1"):gsub("|", "||"):gsub("{{{", "|cffff8855"):gsub("}}}", "|r") local trace = " "..err.stack:gsub("Interface\\AddOns\\", ""):gsub("Interface(\\%w+\\)", "..%1"):gsub(": in function `(.-)'", ": %1()"):gsub(": in function <(.-)>", ":\n %1"):gsub(": in main chunk ", ": "):gsub("\n$",""):gsub("\n", "\n ") local count = err.count if (count > 999) then count = "\226\136\158" --[[Infinity]] end local errPos = id - Swatter.loadCount if errPos <= 0 then errPos = errPos - 1 end Swatter.Error.curError = "|cffff5533Date:|r "..timestamp.."\n|cffff5533ID:|r "..errPos.."\n|cffff5533Error occured in:|r "..context.."\n|cffff5533Count:|r "..count.."\n|cffff5533Message:|r "..message.."\n|cffff5533Debug:|r\n"..trace.."\n|cffff5533AddOns:|r\n"..addlist.."\n" Swatter.Error.selected = false Swatter.ErrorUpdate() Swatter.Error:Show() end function Swatter.ErrorDone() Swatter.Error:Hide() end function Swatter.ErrorPrev() local cur = Swatter.Error.pos or 1 if (cur > 1) then Swatter.ErrorDisplay(cur - 1) else Swatter.ErrorUpdate() end end function Swatter.ErrorNext() local cur = Swatter.Error.pos or 1 local max = table.getn(Swatter.errorOrder) or 0 if (cur < max) then Swatter.ErrorDisplay(cur + 1) else Swatter.ErrorUpdate() end end function Swatter.UpdateNextPrev() local cur = Swatter.Error.pos or 1 local max = #Swatter.errorOrder or 0 if ((max > cur) and (cur ~= -1)) then Swatter.Error.Next:Enable() else Swatter.Error.Next:Disable() end if (cur > 1) then Swatter.Error.Prev:Enable() else Swatter.Error.Prev:Disable() end end function Swatter.ErrorUpdate() if (not Swatter.Error.curError) then Swatter.Error.curError = "" end Swatter.Error.Box:SetText(Swatter.Error.curError) Swatter.Error.Scroll:UpdateScrollChildRect() Swatter.Error.Box:ClearFocus() Swatter.UpdateNextPrev() Swatter.Error:Show() end function Swatter.ErrorClicked() if (Swatter.Error.selected) then return end Swatter.Error.Box:HighlightText() Swatter.Error.selected = true end -- Create our error message frame Swatter.Error = CreateFrame("Frame", "SwatterErrorFrame", UIParent) Swatter.Error:Hide() Swatter.Error:SetPoint("CENTER", "UIParent", "CENTER") Swatter.Error:SetFrameStrata("TOOLTIP") Swatter.Error:SetHeight(280) Swatter.Error:SetWidth(500) Swatter.Error:SetBackdrop({ bgFile = "Interface/Tooltips/ChatBubble-Background", edgeFile = "Interface/Tooltips/ChatBubble-BackDrop", tile = true, tileSize = 32, edgeSize = 32, insets = { left = 32, right = 32, top = 32, bottom = 32 } }) Swatter.Error:SetBackdropColor(0,0,0, 1) Swatter.Error:SetScript("OnShow", Swatter.ErrorShow) Swatter.Error:SetMovable(true) Swatter.Error:SetClampedToScreen(true) Swatter.Error.RealShow = Swatter.Error.Show Swatter.Error.RealHide = Swatter.Error.Hide function Swatter.Error:Show(...) Swatter.Error.isShown = true Swatter.Error:RealShow(...) end function Swatter.Error:Hide(...) Swatter.Error.isShown = nil Swatter.lastShown = #Swatter.errorOrder Swatter.Error:RealHide(...) end Swatter.ProxyFrame = CreateFrame("Frame", "SwatterProxyFrame") Swatter.ProxyFrame:SetParent(Swatter.Error) Swatter.ProxyFrame.IsShown = function() return Swatter.Error:IsShown() end Swatter.ProxyFrame.escCount = 0 Swatter.ProxyFrame.timer = 0 Swatter.ProxyFrame.Hide = ( function( self ) local numEscapes = SwatterData.numEscapes or 1 self.escCount = self.escCount + 1 if ( self.escCount >= numEscapes ) then self:GetParent():Hide() self.escCount = 0 end if ( self.escCount == 1 ) then self.timer = 0 end end ) Swatter.ProxyFrame:SetScript("OnUpdate", function( self, elapsed ) local timer = self.timer + elapsed if ( timer >= 1 ) then self.escCount = 0 end self.timer = timer end ) Swatter.Drag = CreateFrame("Button", nil, Swatter.Error) Swatter.Drag:SetPoint("TOPLEFT", Swatter.Error, "TOPLEFT", 10,-5) Swatter.Drag:SetPoint("TOPRIGHT", Swatter.Error, "TOPRIGHT", -10,-5) Swatter.Drag:SetHeight(8) Swatter.Drag:SetHighlightTexture("Interface\\FriendsFrame\\UI-FriendsFrame-HighlightBar") Swatter.Drag:SetScript("OnMouseDown", function() Swatter.Error:StartMoving() end) Swatter.Drag:SetScript("OnMouseUp", function() Swatter.Error:StopMovingOrSizing() end) Swatter.Error.Done = CreateFrame("Button", "", Swatter.Error, "OptionsButtonTemplate") Swatter.Error.Done:SetText("Close") Swatter.Error.Done:SetPoint("BOTTOMRIGHT", Swatter.Error, "BOTTOMRIGHT", -10, 10) Swatter.Error.Done:SetScript("OnClick", Swatter.ErrorDone) Swatter.Error.Next = CreateFrame("Button", "", Swatter.Error, "OptionsButtonTemplate") Swatter.Error.Next:SetText("Next >") Swatter.Error.Next:SetPoint("BOTTOMRIGHT", Swatter.Error.Done, "BOTTOMLEFT", -5, 0) Swatter.Error.Next:SetScript("OnClick", Swatter.ErrorNext) Swatter.Error.Prev = CreateFrame("Button", "", Swatter.Error, "OptionsButtonTemplate") Swatter.Error.Prev:SetText("< Prev") Swatter.Error.Prev:SetPoint("BOTTOMRIGHT", Swatter.Error.Next, "BOTTOMLEFT", -5, 0) Swatter.Error.Prev:SetScript("OnClick", Swatter.ErrorPrev) Swatter.Error.Mesg = Swatter.Error:CreateFontString("", "OVERLAY", "GameFontNormalSmall") Swatter.Error.Mesg:SetJustifyH("LEFT") Swatter.Error.Mesg:SetPoint("TOPRIGHT", Swatter.Error.Prev, "TOPLEFT", -10, 0) Swatter.Error.Mesg:SetPoint("LEFT", Swatter.Error, "LEFT", 15, 0) Swatter.Error.Mesg:SetHeight(20) Swatter.Error.Mesg:SetText("Select All and Copy the above error message to report this bug.") Swatter.Error.Scroll = CreateFrame("ScrollFrame", "SwatterErrorInputScroll", Swatter.Error, "UIPanelScrollFrameTemplate") Swatter.Error.Scroll:SetPoint("TOPLEFT", Swatter.Error, "TOPLEFT", 20, -20) Swatter.Error.Scroll:SetPoint("RIGHT", Swatter.Error, "RIGHT", -30, 0) Swatter.Error.Scroll:SetPoint("BOTTOM", Swatter.Error.Done, "TOP", 0, 10) Swatter.Error.Box = CreateFrame("EditBox", "SwatterErrorEditBox", Swatter.Error.Scroll) Swatter.Error.Box:SetWidth(450) Swatter.Error.Box:SetHeight(85) Swatter.Error.Box:SetMultiLine(true) Swatter.Error.Box:SetAutoFocus(false) Swatter.Error.Box:SetFontObject(GameFontHighlight) Swatter.Error.Box:SetScript("OnEscapePressed", Swatter.ErrorDone) Swatter.Error.Box:SetScript("OnTextChanged", Swatter.ErrorUpdate) Swatter.Error.Box:SetScript("OnEditFocusGained", Swatter.ErrorClicked) Swatter.Error.Scroll:SetScrollChild(Swatter.Error.Box) seterrorhandler(Swatter.OnError) Swatter.Frame = CreateFrame("Frame") Swatter.Frame:Show() Swatter.Frame:SetScript("OnEvent", Swatter.OnEvent) Swatter.Frame:RegisterEvent("ADDON_LOADED") Swatter.Frame:RegisterEvent("ADDON_ACTION_FORBIDDEN") Swatter.Frame:RegisterEvent("ADDON_ACTION_BLOCKED") Swatter.Frame:RegisterEvent("PLAYER_ENTERING_WORLD") Swatter.Frame:RegisterEvent("PLAYER_LOGIN") Swatter.Frame:SetScript("OnUpdate", function(self, elapsed) if not self.timer then self.timer = 0 end self.timer = self.timer + elapsed if self.timer > 2 then self.timer = 0 if Swatter.Error.isShown then if not Swatter.Error:IsVisible() then Swatter.Error:RealShow() end elseif Swatter.Error:IsVisible() then Swatter.Error:RealHide() end end end) SetItemRef = Swatter.SetItemRef UIParent:UnregisterEvent("ADDON_ACTION_FORBIDDEN") UIParent:UnregisterEvent("ADDON_ACTION_BLOCKED") SLASH_SWATTER1 = "/swatter" SLASH_SWATTER2 = "/swat" SlashCmdList["SWATTER"] = function(msg) if (not msg or msg == "" or msg == "help") then chat("Swatter help:") chat(" /swat enable - Enables swatter") chat(" /swat disable - Disables swatter") chat(" /swat show - Shows the last error box again") chat(" /swat autoshow - Enables swatter autopopup upon error") chat(" /swat noauto - Swatter will only show an error in chat") chat(" /swat warn - Enables swatter's blocked warnings") chat(" /swat nowarn - Disables swatter's blocked warnings") chat(" /swat clear - Swatter will clear the list of errors") elseif (msg == "show") then Swatter.ErrorShow() elseif (msg == "enable") then SwatterData.enabled = true chat("Swatter will now catch errors") elseif (msg == "disable") then SwatterData.enabled = false chat("Swatter will no longer catch errors") elseif (msg == "warn") then SwatterData.warning = true chat("Swatter will now catch warnings") elseif (msg == "nowarn") then SwatterData.warning = false chat("Swatter will no longer catch warnings") elseif (msg == "autoshow") then SwatterData.autoshow = true chat("Swatter will popup the first time it sees an error") elseif (msg == "noauto") then SwatterData.autoshow = false chat("Swatter will print into chat instead of popping up") elseif (msg == "clear") then Swatter.Error:Hide() SwatterData.errors = {} Swatter.errorOrder = {} Swatter.loadCount = 0 Swatter.lastShown = 0 --Note: we are not killing the frame.Swatter values - I am hoping that they are transient to the game session and aren't saved anywhere --Swatter.ErrorUpdate() chat("Swatter errors have been cleared") end end addSlideIcon()