1
0
forked from 0ad/0ad
0ad/source/scriptinterface/ThreadDebugger.cpp
2014-01-05 13:58:38 +00:00

940 lines
30 KiB
C++

/* Copyright (C) 2013 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
// JS debugger temporarily disabled during the SpiderMonkey upgrade (check trac ticket #2348 for details)
#include "precompiled.h" // needs to be here for Windows builds
/*
#include "ThreadDebugger.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include <map>
#include <queue>
// Hooks
CMutex ThrowHandlerMutex;
static JSTrapStatus ThrowHandler_(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, void* closure)
{
CScopeLock lock(ThrowHandlerMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) closure;
return pThreadDebugger->ThrowHandler(cx, script, pc, rval);
}
CMutex TrapHandlerMutex;
static JSTrapStatus TrapHandler_(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, jsval closure)
{
CScopeLock lock(TrapHandlerMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) JSVAL_TO_PRIVATE(closure);
jsval val = JSVAL_NULL;
return pThreadDebugger->TrapHandler(cx, script, pc, rval, val);
}
CMutex StepHandlerMutex;
JSTrapStatus StepHandler_(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, void* closure)
{
CScopeLock lock(StepHandlerMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) closure;
jsval val = JSVAL_VOID;
return pThreadDebugger->StepHandler(cx, script, pc, rval, &val);
}
CMutex StepIntoHandlerMutex;
JSTrapStatus StepIntoHandler_(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, void* closure)
{
CScopeLock lock(StepIntoHandlerMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) closure;
return pThreadDebugger->StepIntoHandler(cx, script, pc, rval, NULL);
}
CMutex NewScriptHookMutex;
void NewScriptHook_(JSContext* cx, const char* filename, unsigned lineno, JSScript* script, JSFunction* fun, void* callerdata)
{
CScopeLock lock(NewScriptHookMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) callerdata;
return pThreadDebugger->NewScriptHook(cx, filename, lineno, script, fun, NULL);
}
CMutex DestroyScriptHookMutex;
void DestroyScriptHook_(JSContext* cx, JSScript* script, void* callerdata)
{
CScopeLock lock(DestroyScriptHookMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) callerdata;
return pThreadDebugger->DestroyScriptHook(cx, script);
}
CMutex StepOutHandlerMutex;
JSTrapStatus StepOutHandler_(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, void* closure)
{
CScopeLock lock(StepOutHandlerMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) closure;
return pThreadDebugger->StepOutHandler(cx, script, pc, rval, NULL);
}
CMutex CheckForBreakRequestHandlerMutex;
JSTrapStatus CheckForBreakRequestHandler_(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, void* closure)
{
CScopeLock lock(CheckForBreakRequestHandlerMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) closure;
return pThreadDebugger->CheckForBreakRequestHandler(cx, script, pc, rval, NULL);
}
CMutex CallHookMutex;
static void* CallHook_(JSContext* cx, JSStackFrame* fp, JSBool before, JSBool* UNUSED(ok), void* closure)
{
CScopeLock lock(CallHookMutex);
CThreadDebugger* pThreadDebugger = (CThreadDebugger*) closure;
if (before)
{
JSScript* script;
script = JS_GetFrameScript(cx, fp);
const char* fileName = JS_GetScriptFilename(cx, script);
uint lineno = JS_GetScriptBaseLineNumber(cx, script);
JSFunction* fun = JS_GetFrameFunction(cx, fp);
pThreadDebugger->ExecuteHook(cx, fileName, lineno, script, fun, closure);
}
return closure;
}
/// ThreadDebugger_impl
struct StackInfoRequest
{
STACK_INFO requestType;
uint nestingLevel;
SDL_sem* semaphore;
};
struct trapLocation
{
jsbytecode* pBytecode;
JSScript* pScript;
uint firstLineInFunction;
uint lastLineInFunction;
};
struct ThreadDebugger_impl
{
NONCOPYABLE(ThreadDebugger_impl);
public:
ThreadDebugger_impl();
~ThreadDebugger_impl();
CMutex m_Mutex;
CMutex m_ActiveBreakpointsMutex;
CMutex m_NextDbgCmdMutex;
CMutex m_IsInBreakMutex;
// This member could actually be used by other threads via CompareScriptInterfacePtr(), but that should be safe
ScriptInterface* m_pScriptInterface;
CDebuggingServer* m_pDebuggingServer;
// We store the pointer on the heap because the stack frame becomes invalid in certain cases
// and spidermonkey throws errors if it detects a pointer on the stack.
// We only use the pointer for comparing it with the current stack pointer and we don't try to access it, so it
// shouldn't be a problem.
JSStackFrame** m_pLastBreakFrame;
uint m_ObjectReferenceID;
/// shared between multiple mongoose threads and one scriptinterface thread
std::string m_BreakFileName;
uint m_LastBreakLine;
bool m_IsInBreak;
DBGCMD m_NextDbgCmd;
std::queue<StackInfoRequest> m_StackInfoRequests;
std::map<std::string, std::map<uint, trapLocation> > m_LineToPCMap;
std::list<CActiveBreakPoint*> m_ActiveBreakPoints;
std::map<STACK_INFO, std::map<uint, std::string> > m_StackFrameData;
std::string m_Callstack;
/// shared between multiple mongoose threads (initialization may be an exception)
std::string m_Name;
uint m_ID;
};
ThreadDebugger_impl::ThreadDebugger_impl()
: m_NextDbgCmd(DBG_CMD_NONE)
, m_pScriptInterface(NULL)
, m_pDebuggingServer(NULL)
, m_pLastBreakFrame(new JSStackFrame*)
, m_IsInBreak(false)
{ }
ThreadDebugger_impl::~ThreadDebugger_impl()
{
delete m_pLastBreakFrame;
}
/// CThreadDebugger
void CThreadDebugger::ClearTrapsToRemove()
{
CScopeLock lock(m->m_Mutex);
std::list<CActiveBreakPoint*>::iterator itr=m->m_ActiveBreakPoints.begin();
while (itr != m->m_ActiveBreakPoints.end())
{
if ((*itr)->m_ToRemove)
{
ClearTrap((*itr));
// Remove the breakpoint
delete (*itr);
itr = m->m_ActiveBreakPoints.erase(itr);
}
else
++itr;
}
}
void CThreadDebugger::ClearTrap(CActiveBreakPoint* activeBreakPoint)
{
ENSURE(activeBreakPoint->m_Script != NULL && activeBreakPoint->m_Pc != NULL);
JSTrapHandler prevHandler;
jsval prevClosure;
JS_ClearTrap(m->m_pScriptInterface->GetContext(), activeBreakPoint->m_Script, activeBreakPoint->m_Pc, &prevHandler, &prevClosure);
activeBreakPoint->m_Script = NULL;
activeBreakPoint->m_Pc = NULL;
}
void CThreadDebugger::SetAllNewTraps()
{
std::list<CBreakPoint>* pBreakPoints = NULL;
double breakPointsLockID;
breakPointsLockID = m->m_pDebuggingServer->AquireBreakPointAccess(&pBreakPoints);
std::list<CBreakPoint>::iterator itr = pBreakPoints->begin();
while (itr != pBreakPoints->end())
{
if (CheckIfMappingPresent((*itr).m_Filename, (*itr).m_UserLine))
{
// We must not set a new trap if we already have set a trap for this line of code.
// For lines without source code it's possible to have breakpoints set that actually refer to another line
// that contains code. This situation is possible if the line containing the sourcecode already has a breakpoint
// set and the user sets another one by setting a breakpoint on a line directly above without sourcecode.
bool trapAlreadySet = false;
{
CScopeLock lock(m->m_Mutex);
std::list<CActiveBreakPoint*>::iterator itr1;
for (itr1 = m->m_ActiveBreakPoints.begin(); itr1 != m->m_ActiveBreakPoints.end(); ++itr1)
{
if ((*itr1)->m_ActualLine == (*itr).m_UserLine)
trapAlreadySet = true;
}
}
if (!trapAlreadySet)
{
CActiveBreakPoint* pActiveBreakPoint = new CActiveBreakPoint((*itr));
SetNewTrap(pActiveBreakPoint, (*itr).m_Filename, (*itr).m_UserLine);
{
CScopeLock lock(m->m_Mutex);
m->m_ActiveBreakPoints.push_back(pActiveBreakPoint);
}
itr = pBreakPoints->erase(itr);
continue;
}
}
++itr;
}
m->m_pDebuggingServer->ReleaseBreakPointAccess(breakPointsLockID);
}
bool CThreadDebugger::CheckIfMappingPresent(std::string filename, uint line)
{
bool isPresent = (m->m_LineToPCMap.end() != m->m_LineToPCMap.find(filename) && m->m_LineToPCMap[filename].end() != m->m_LineToPCMap[filename].find(line));
return isPresent;
}
void CThreadDebugger::SetNewTrap(CActiveBreakPoint* activeBreakPoint, std::string filename, uint line)
{
ENSURE(activeBreakPoint->m_Script == NULL); // The trap must not be set already!
ENSURE(CheckIfMappingPresent(filename, line)); // You have to check if the mapping exists before calling this function!
jsbytecode* pc = m->m_LineToPCMap[filename][line].pBytecode;
JSScript* script = m->m_LineToPCMap[filename][line].pScript;
activeBreakPoint->m_Script = script;
activeBreakPoint->m_Pc = pc;
ENSURE(script != NULL && pc != NULL);
activeBreakPoint->m_ActualLine = JS_PCToLineNumber(m->m_pScriptInterface->GetContext(), script, pc);
JS_SetTrap(m->m_pScriptInterface->GetContext(), script, pc, TrapHandler_, PRIVATE_TO_JSVAL(this));
}
CThreadDebugger::CThreadDebugger() :
m(new ThreadDebugger_impl())
{
}
CThreadDebugger::~CThreadDebugger()
{
// Clear all Traps and Breakpoints that are marked for removal
ClearTrapsToRemove();
// Return all breakpoints to the associated CDebuggingServer
ReturnActiveBreakPoints(NULL);
// Remove all the hooks because they store a pointer to this object
JS_SetExecuteHook(m->m_pScriptInterface->GetJSRuntime(), NULL, NULL);
JS_SetCallHook(m->m_pScriptInterface->GetJSRuntime(), NULL, NULL);
JS_SetNewScriptHook(m->m_pScriptInterface->GetJSRuntime(), NULL, NULL);
JS_SetDestroyScriptHook(m->m_pScriptInterface->GetJSRuntime(), NULL, NULL);
}
void CThreadDebugger::ReturnActiveBreakPoints(jsbytecode* pBytecode)
{
CScopeLock lock(m->m_ActiveBreakpointsMutex);
std::list<CActiveBreakPoint*>::iterator itr;
itr = m->m_ActiveBreakPoints.begin();
while (itr != m->m_ActiveBreakPoints.end())
{
// Breakpoints marked for removal should be deleted instead of returned!
if ( ((*itr)->m_Pc == pBytecode || pBytecode == NULL) && !(*itr)->m_ToRemove )
{
std::list<CBreakPoint>* pBreakPoints;
double breakPointsLockID = m->m_pDebuggingServer->AquireBreakPointAccess(&pBreakPoints);
CBreakPoint breakPoint;
breakPoint.m_UserLine = (*itr)->m_UserLine;
breakPoint.m_Filename = (*itr)->m_Filename;
// All active breakpoints should have a trap set
ClearTrap((*itr));
pBreakPoints->push_back(breakPoint);
delete (*itr);
itr = m->m_ActiveBreakPoints.erase(itr);
m->m_pDebuggingServer->ReleaseBreakPointAccess(breakPointsLockID);
}
else
++itr;
}
}
void CThreadDebugger::Initialize(uint id, std::string name, ScriptInterface* pScriptInterface, CDebuggingServer* pDebuggingServer)
{
ENSURE(id != 0);
m->m_ID = id;
m->m_Name = name;
m->m_pScriptInterface = pScriptInterface;
m->m_pDebuggingServer = pDebuggingServer;
JS_SetExecuteHook(m->m_pScriptInterface->GetJSRuntime(), CallHook_, (void*)this);
JS_SetCallHook(m->m_pScriptInterface->GetJSRuntime(), CallHook_, (void*)this);
JS_SetNewScriptHook(m->m_pScriptInterface->GetJSRuntime(), NewScriptHook_, (void*)this);
JS_SetDestroyScriptHook(m->m_pScriptInterface->GetJSRuntime(), DestroyScriptHook_, (void*)this);
JS_SetThrowHook(m->m_pScriptInterface->GetJSRuntime(), ThrowHandler_, (void*)this);
if (m->m_pDebuggingServer->GetSettingSimultaneousThreadBreak())
{
// Setup a handler to check for break-requests from the DebuggingServer regularly
JS_SetInterrupt(m->m_pScriptInterface->GetJSRuntime(), CheckForBreakRequestHandler_, (void*)this);
}
}
JSTrapStatus CThreadDebugger::StepHandler(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* rval, void* UNUSED(closure))
{
// We break in two conditions
// 1. We are in the same frame but on a different line
// Note: On loops for example, we can go a few lines up again without leaving the current stack frame, so it's not necessarily
// a higher line number.
// 2. We are in a different Frame and m_pLastBreakFrame is not a parent of the current frame (because we stepped out of the function)
uint line = JS_PCToLineNumber(cx, script, pc);
JSStackFrame* iter = NULL;
JSStackFrame* pStackFrame;
pStackFrame = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
uint lastBreakLine = GetLastBreakLine() ;
jsval val = JSVAL_VOID;
if ((*m->m_pLastBreakFrame == pStackFrame && lastBreakLine != line) ||
(*m->m_pLastBreakFrame != pStackFrame && !CurrentFrameIsChildOf(*m->m_pLastBreakFrame)))
return BreakHandler(cx, script, pc, rval, val, BREAK_SRC_INTERRUP);
else
return JSTRAP_CONTINUE;
}
JSTrapStatus CThreadDebugger::StepIntoHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void* UNUSED(closure))
{
// We break when we are on the same stack frame but not on the same line
// or when we are on another stack frame.
uint line = JS_PCToLineNumber(cx, script, pc);
JSStackFrame* iter = NULL;
JSStackFrame* pStackFrame;
pStackFrame = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
uint lastBreakLine = GetLastBreakLine();
jsval val = JSVAL_VOID;
if ((*m->m_pLastBreakFrame == pStackFrame && lastBreakLine != line) || *m->m_pLastBreakFrame != pStackFrame)
return BreakHandler(cx, script, pc, rval, val, BREAK_SRC_INTERRUP);
else
return JSTRAP_CONTINUE;
}
JSTrapStatus CThreadDebugger::StepOutHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void* UNUSED(closure))
{
// We break when we are in a different Frame and m_pLastBreakFrame is not a parent of the current frame
// (because we stepped out of the function)
JSStackFrame* iter = NULL;
JSStackFrame* pStackFrame;
pStackFrame = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
if (pStackFrame != *m->m_pLastBreakFrame && !CurrentFrameIsChildOf(*m->m_pLastBreakFrame))
{
jsval val = JSVAL_VOID;
return BreakHandler(cx, script, pc, rval, val, BREAK_SRC_INTERRUP);
}
else
return JSTRAP_CONTINUE;
}
bool CThreadDebugger::CurrentFrameIsChildOf(JSStackFrame* pParentFrame)
{
JSStackFrame* iter = NULL;
JSStackFrame* fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
// Get the first parent Frame
fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
while (fp)
{
if (fp == pParentFrame)
return true;
fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
}
return false;
}
JSTrapStatus CThreadDebugger::CheckForBreakRequestHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void* UNUSED(closure))
{
jsval val = JSVAL_VOID;
if (m->m_pDebuggingServer->GetBreakRequestedByThread() || m->m_pDebuggingServer->GetBreakRequestedByUser())
return BreakHandler(cx, script, pc, rval, val, BREAK_SRC_INTERRUP);
else
return JSTRAP_CONTINUE;
}
JSTrapStatus CThreadDebugger::TrapHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, jsval UNUSED(closure))
{
jsval val = JSVAL_NULL;
return BreakHandler(cx, script, pc, rval, val, BREAK_SRC_TRAP);
}
JSTrapStatus CThreadDebugger::ThrowHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval)
{
jsval jsexception;
JS_GetPendingException(cx, &jsexception);
if (JSVAL_IS_STRING(jsexception))
{
std::string str(JS_EncodeString(cx, JSVAL_TO_STRING(jsexception)));
if (str == "Breakpoint" || m->m_pDebuggingServer->GetSettingBreakOnException())
{
if (str == "Breakpoint")
JS_ClearPendingException(cx);
jsval val = JSVAL_NULL;
return BreakHandler(cx, script, pc, rval, val, BREAK_SRC_EXCEPTION);
}
}
return JSTRAP_CONTINUE;
}
JSTrapStatus CThreadDebugger::BreakHandler(JSContext* cx, JSScript* script, jsbytecode* pc, jsval* UNUSED(rval), jsval UNUSED(closure), BREAK_SRC breakSrc)
{
uint line = JS_PCToLineNumber(cx, script, pc);
std::string filename(JS_GetScriptFilename(cx, script));
SetIsInBreak(true);
SaveCallstack();
SetLastBreakLine(line);
SetBreakFileName(filename);
*m->m_pLastBreakFrame = NULL;
if (breakSrc == BREAK_SRC_INTERRUP)
{
JS_ClearInterrupt(m->m_pScriptInterface->GetJSRuntime(), NULL, NULL);
JS_SetSingleStepMode(cx, script, false);
}
if (m->m_pDebuggingServer->GetSettingSimultaneousThreadBreak())
{
m->m_pDebuggingServer->SetBreakRequestedByThread(true);
}
// Wait until the user continues the execution
while (1)
{
DBGCMD nextDbgCmd = GetNextDbgCmd();
while (!m->m_StackInfoRequests.empty())
{
StackInfoRequest request = m->m_StackInfoRequests.front();
SaveStackFrameData(request.requestType, request.nestingLevel);
SDL_SemPost(request.semaphore);
m->m_StackInfoRequests.pop();
}
if (nextDbgCmd == DBG_CMD_NONE)
{
// Wait a while before checking for new m_NextDbgCmd again.
// We don't want this loop to take 100% of a CPU core for each thread that is in break mode.
// On the other hande we don't want the debugger to become unresponsive.
SDL_Delay(100);
}
else if (nextDbgCmd == DBG_CMD_SINGLESTEP || nextDbgCmd == DBG_CMD_STEPINTO || nextDbgCmd == DBG_CMD_STEPOUT)
{
JSStackFrame* iter = NULL;
*m->m_pLastBreakFrame = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
if (!JS_SetSingleStepMode(cx, script, true))
LOGERROR(L"JS_SetSingleStepMode returned false!"); // TODO: When can this happen?
else
{
if (nextDbgCmd == DBG_CMD_SINGLESTEP)
{
JS_SetInterrupt(m->m_pScriptInterface->GetJSRuntime(), StepHandler_, this);
break;
}
else if (nextDbgCmd == DBG_CMD_STEPINTO)
{
JS_SetInterrupt(m->m_pScriptInterface->GetJSRuntime(), StepIntoHandler_, this);
break;
}
else if (nextDbgCmd == DBG_CMD_STEPOUT)
{
JS_SetInterrupt(m->m_pScriptInterface->GetJSRuntime(), StepOutHandler_, this);
break;
}
}
}
else if (nextDbgCmd == DBG_CMD_CONTINUE)
{
if (!JS_SetSingleStepMode(cx, script, true))
LOGERROR(L"JS_SetSingleStepMode returned false!"); // TODO: When can this happen?
else
{
// Setup a handler to check for break-requests from the DebuggingServer regularly
JS_SetInterrupt(m->m_pScriptInterface->GetJSRuntime(), CheckForBreakRequestHandler_, this);
}
break;
}
else
debug_warn("Invalid DBGCMD found in CThreadDebugger::BreakHandler!");
}
ClearTrapsToRemove();
SetAllNewTraps();
SetNextDbgCmd(DBG_CMD_NONE);
SetIsInBreak(false);
SetBreakFileName(std::string());
// All saved stack data becomes invalid
{
CScopeLock lock(m->m_Mutex);
m->m_StackFrameData.clear();
}
return JSTRAP_CONTINUE;
}
void CThreadDebugger::NewScriptHook(JSContext* cx, const char* filename, unsigned lineno, JSScript* script, JSFunction* UNUSED(fun), void* UNUSED(callerdata))
{
uint scriptExtent = JS_GetScriptLineExtent (cx, script);
std::string stringFileName(filename);
if (stringFileName.empty())
return;
for (uint line = lineno; line < scriptExtent + lineno; ++line)
{
// If we already have a mapping for this line, we check if the current scipt is more deeply nested.
// If it isn't more deeply nested, we don't overwrite the previous mapping
// The most deeply nested script is always the one that must be used!
uint firstLine = 0;
uint lastLine = 0;
jsbytecode* oldPC = NULL;
if (CheckIfMappingPresent(stringFileName, line))
{
firstLine = m->m_LineToPCMap[stringFileName][line].firstLineInFunction;
lastLine = m->m_LineToPCMap[stringFileName][line].lastLineInFunction;
// If an entry nested equally is present too, we must overwrite it.
// The same script(function) can trigger a NewScriptHook multiple times without DestroyScriptHooks between these
// calls. In this case the old script becomes invalid.
if (lineno < firstLine || scriptExtent + lineno > lastLine)
continue;
else
oldPC = m->m_LineToPCMap[stringFileName][line].pBytecode;
}
jsbytecode* pc = JS_LineNumberToPC (cx, script, line);
m->m_LineToPCMap[stringFileName][line].pBytecode = pc;
m->m_LineToPCMap[stringFileName][line].pScript = script;
m->m_LineToPCMap[stringFileName][line].firstLineInFunction = lineno;
m->m_LineToPCMap[stringFileName][line].lastLineInFunction = lineno + scriptExtent;
// If we are replacing a script, the associated traps become invalid
if (lineno == firstLine && scriptExtent + lineno == lastLine)
{
ReturnActiveBreakPoints(oldPC);
SetAllNewTraps();
}
}
}
void CThreadDebugger::DestroyScriptHook(JSContext* cx, JSScript* script)
{
uint scriptExtent = JS_GetScriptLineExtent (cx, script);
uint baseLine = JS_GetScriptBaseLineNumber(cx, script);
char* pStr = NULL;
pStr = (char*)JS_GetScriptFilename(cx, script);
if (pStr != NULL)
{
std::string fileName(pStr);
for (uint line = baseLine; line < scriptExtent + baseLine; ++line)
{
if (CheckIfMappingPresent(fileName, line))
{
if (m->m_LineToPCMap[fileName][line].pScript == script)
{
ReturnActiveBreakPoints(m->m_LineToPCMap[fileName][line].pBytecode);
m->m_LineToPCMap[fileName].erase(line);
if (m->m_LineToPCMap[fileName].empty())
m->m_LineToPCMap.erase(fileName);
}
}
}
}
}
void CThreadDebugger::ExecuteHook(JSContext* UNUSED(cx), const char* UNUSED(filename), unsigned UNUSED(lineno), JSScript* UNUSED(script), JSFunction* UNUSED(fun), void* UNUSED(callerdata))
{
// Search all breakpoints that have no trap set yet
{
PROFILE2("ExecuteHook");
SetAllNewTraps();
}
return;
}
bool CThreadDebugger::ToggleBreakPoint(std::string filename, uint userLine)
{
CScopeLock lock(m->m_Mutex);
std::list<CActiveBreakPoint*>::iterator itr;
for (itr = m->m_ActiveBreakPoints.begin(); itr != m->m_ActiveBreakPoints.end(); ++itr)
{
if ((*itr)->m_UserLine == userLine && (*itr)->m_Filename == filename)
{
(*itr)->m_ToRemove = !(*itr)->m_ToRemove;
return true;
}
}
return false;
}
void CThreadDebugger::GetCallstack(std::stringstream& response)
{
CScopeLock lock(m->m_Mutex);
response << m->m_Callstack;
}
void CThreadDebugger::SaveCallstack()
{
ENSURE(GetIsInBreak());
CScopeLock lock(m->m_Mutex);
JSStackFrame *fp;
JSStackFrame *iter = 0;
jsint counter = 0;
JSObject* jsArray;
jsArray = JS_NewArrayObject(m->m_pScriptInterface->GetContext(), 0, 0);
JSString* functionID;
fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
while (fp)
{
JSFunction* fun = 0;
fun = JS_GetFrameFunction(m->m_pScriptInterface->GetContext(), fp);
if (NULL == fun)
functionID = JS_NewStringCopyZ(m->m_pScriptInterface->GetContext(), "null");
else
{
functionID = JS_GetFunctionId(fun);
if (NULL == functionID)
functionID = JS_NewStringCopyZ(m->m_pScriptInterface->GetContext(), "anonymous");
}
JSBool ret = JS_DefineElement(m->m_pScriptInterface->GetContext(), jsArray, counter, STRING_TO_JSVAL(functionID), NULL, NULL, 0);
ENSURE(ret);
fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
counter++;
}
m->m_Callstack.clear();
m->m_Callstack = m->m_pScriptInterface->StringifyJSON(OBJECT_TO_JSVAL(jsArray), false).c_str();
}
void CThreadDebugger::GetStackFrameData(std::stringstream& response, uint nestingLevel, STACK_INFO stackInfoKind)
{
// If the data is not yet cached, request it and wait until it's ready.
bool dataCached = false;
{
CScopeLock lock(m->m_Mutex);
dataCached = (!m->m_StackFrameData.empty() && m->m_StackFrameData[stackInfoKind].end() != m->m_StackFrameData[stackInfoKind].find(nestingLevel));
}
if (!dataCached)
{
SDL_sem* semaphore = SDL_CreateSemaphore(0);
AddStackInfoRequest(stackInfoKind, nestingLevel, semaphore);
SDL_SemWait(semaphore);
SDL_DestroySemaphore(semaphore);
}
CScopeLock lock(m->m_Mutex);
{
response.str(std::string());
response << m->m_StackFrameData[stackInfoKind][nestingLevel];
}
}
void CThreadDebugger::AddStackInfoRequest(STACK_INFO requestType, uint nestingLevel, SDL_sem* semaphore)
{
StackInfoRequest request;
request.requestType = requestType;
request.semaphore = semaphore;
request.nestingLevel = nestingLevel;
m->m_StackInfoRequests.push(request);
}
void CThreadDebugger::SaveStackFrameData(STACK_INFO stackInfo, uint nestingLevel)
{
ENSURE(GetIsInBreak());
CScopeLock lock(m->m_Mutex);
JSStackFrame *iter = 0;
uint counter = 0;
jsval val;
if (stackInfo == STACK_INFO_GLOBALOBJECT)
{
JSObject* obj;
obj = JS_GetGlobalForScopeChain(m->m_pScriptInterface->GetContext());
m->m_StackFrameData[stackInfo][nestingLevel] = StringifyCyclicJSON(OBJECT_TO_JSVAL(obj), false);
}
else
{
JSStackFrame *fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
while (fp)
{
if (counter == nestingLevel)
{
if (stackInfo == STACK_INFO_LOCALS)
{
JSObject* obj;
obj = JS_GetFrameCallObject(m->m_pScriptInterface->GetContext(), fp);
//obj = JS_GetFrameScopeChain(m->m_pScriptInterface->GetContext(), fp);
m->m_StackFrameData[stackInfo][nestingLevel] = StringifyCyclicJSON(OBJECT_TO_JSVAL(obj), false);
}
else if (stackInfo == STACK_INFO_THIS)
{
if (JS_GetFrameThis(m->m_pScriptInterface->GetContext(), fp, &val))
m->m_StackFrameData[stackInfo][nestingLevel] = StringifyCyclicJSON(val, false);
else
m->m_StackFrameData[stackInfo][nestingLevel] = "";
}
}
counter++;
fp = JS_FrameIterator(m->m_pScriptInterface->GetContext(), &iter);
}
}
}
*/
/*
* TODO: This is very hacky and ugly and should be improved.
* It replaces cyclic references with a notification that cyclic references are not supported.
* It would be better to create a format that supports cyclic references and allows the UI to display them correctly.
* Unfortunately this seems to require writing (or embedding) a new serializer to JSON or something similar.
*
* Some things about the implementation which aren't optimal:
* 1. It uses global variables (they are limited to a namespace though).
* 2. It has to work around a bug in Spidermonkey.
* 3. It copies code from CScriptInterface. I did this to separate it cleanly because the debugger should not affect
* the rest of the game and because this part of code should be replaced anyway in the future.
*/
/*
namespace CyclicRefWorkaround
{
std::set<JSObject*> g_ProcessedObjects;
jsval g_LastKey;
jsval g_LastValue;
bool g_RecursionDetectedInPrevReplacer = false;
uint g_countSameKeys = 0;
struct Stringifier
{
static JSBool callback(const jschar* buf, uint32 len, void* data)
{
utf16string str(buf, buf+len);
std::wstring strw(str.begin(), str.end());
Status err; // ignore Unicode errors
static_cast<Stringifier*>(data)->stream << utf8_from_wstring(strw, &err);
return JS_TRUE;
}
std::stringstream stream;
};
JSBool replacer(JSContext* cx, uintN UNUSED(argc), jsval* vp)
{
jsval value = JS_ARGV(cx, vp)[1];
jsval key = JS_ARGV(cx, vp)[0];
if (g_LastKey == key)
g_countSameKeys++;
else
g_countSameKeys = 0;
if (JSVAL_IS_OBJECT(value))
{
// Work around a spidermonkey bug that causes replacer to be called twice with the same key:
// https://bugzilla.mozilla.org/show_bug.cgi?id=636079
// TODO: Remove the workaround as soon as we upgrade to a newer version of Spidermonkey.
if (g_ProcessedObjects.end() == g_ProcessedObjects.find(JSVAL_TO_OBJECT(value)))
{
g_ProcessedObjects.insert(JSVAL_TO_OBJECT(value));
}
else if (g_countSameKeys %2 == 0 || g_RecursionDetectedInPrevReplacer)
{
g_RecursionDetectedInPrevReplacer = true;
jsval ret = STRING_TO_JSVAL(JS_NewStringCopyZ(cx, "Debugger: object removed from output because of cyclic reference."));
JS_SET_RVAL(cx, vp, ret);
g_LastKey = key;
g_LastValue = value;
return JS_TRUE;
}
}
g_LastKey = key;
g_LastValue = value;
g_RecursionDetectedInPrevReplacer = false;
JS_SET_RVAL(cx, vp, JS_ARGV(cx, vp)[1]);
return JS_TRUE;
}
}
std::string CThreadDebugger::StringifyCyclicJSON(jsval obj, bool indent)
{
CyclicRefWorkaround::Stringifier str;
CyclicRefWorkaround::g_ProcessedObjects.clear();
CyclicRefWorkaround::g_LastKey = JSVAL_VOID;
JSObject* pGlob = JSVAL_TO_OBJECT(m->m_pScriptInterface->GetGlobalObject());
JSFunction* fun = JS_DefineFunction(m->m_pScriptInterface->GetContext(), pGlob, "replacer", CyclicRefWorkaround::replacer, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
JSObject* replacer = JS_GetFunctionObject(fun);
if (!JS_Stringify(m->m_pScriptInterface->GetContext(), &obj, replacer, indent ? INT_TO_JSVAL(2) : JSVAL_VOID, &CyclicRefWorkaround::Stringifier::callback, &str))
{
LOGERROR(L"StringifyJSON failed");
jsval exec;
jsval execString;
if (JS_GetPendingException(m->m_pScriptInterface->GetContext(), &exec))
{
if (JSVAL_IS_OBJECT(exec))
{
JS_GetProperty(m->m_pScriptInterface->GetContext(), JSVAL_TO_OBJECT(exec), "message", &execString);
if (JSVAL_IS_STRING(execString))
{
std::string strExec = JS_EncodeString(m->m_pScriptInterface->GetContext(), JSVAL_TO_STRING(execString));
LOGERROR(L"Error: %hs", strExec.c_str());
}
}
}
JS_ClearPendingException(m->m_pScriptInterface->GetContext());
return std::string();
}
return str.stream.str();
}
bool CThreadDebugger::CompareScriptInterfacePtr(ScriptInterface* pScriptInterface) const
{
return (pScriptInterface == m->m_pScriptInterface);
}
std::string CThreadDebugger::GetBreakFileName()
{
CScopeLock lock(m->m_Mutex);
return m->m_BreakFileName;
}
void CThreadDebugger::SetBreakFileName(std::string breakFileName)
{
CScopeLock lock(m->m_Mutex);
m->m_BreakFileName = breakFileName;
}
uint CThreadDebugger::GetLastBreakLine()
{
CScopeLock lock(m->m_Mutex);
return m->m_LastBreakLine;
}
void CThreadDebugger::SetLastBreakLine(uint breakLine)
{
CScopeLock lock(m->m_Mutex);
m->m_LastBreakLine = breakLine;
}
bool CThreadDebugger::GetIsInBreak()
{
CScopeLock lock(m->m_IsInBreakMutex);
return m->m_IsInBreak;
}
void CThreadDebugger::SetIsInBreak(bool isInBreak)
{
CScopeLock lock(m->m_IsInBreakMutex);
m->m_IsInBreak = isInBreak;
}
void CThreadDebugger::SetNextDbgCmd(DBGCMD dbgCmd)
{
CScopeLock lock(m->m_NextDbgCmdMutex);
m->m_NextDbgCmd = dbgCmd;
}
DBGCMD CThreadDebugger::GetNextDbgCmd()
{
CScopeLock lock(m->m_NextDbgCmdMutex);
return m->m_NextDbgCmd;
}
std::string CThreadDebugger::GetName()
{
CScopeLock lock(m->m_Mutex);
return m->m_Name;
}
uint CThreadDebugger::GetID()
{
CScopeLock lock(m->m_Mutex);
return m->m_ID;
}
*/