1
0
forked from 0ad/0ad

Adds the server-side part of the javascript debugger. Refs #410

This was SVN commit r13238.
This commit is contained in:
Yves 2013-03-07 13:49:49 +00:00
parent f5be596ee8
commit 73951b75fc
16 changed files with 1929 additions and 52 deletions

View File

@ -290,11 +290,15 @@ hotkey.profile.save = "Shift+F11" ; Save current profiler data to logs
hotkey.profile2.enable = "F11" ; Enable HTTP/GPU modes for new profiler
profiler2.http.autoenable = false ; Enable HTTP server output at startup (default off for security/performance)
profiler2.script.enable = false ; Enable Javascript profiling. Needs to be set before startup and can't be changed later. (default off for performance)
profiler2.gpu.autoenable = false ; Enable GPU timing at startup (default off for performance/compatibility)
profiler2.gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available
profiler2.gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available
profiler2.gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available
; > JS DEBUGGER
jsdebugger.enable = false ; Enable Javascript debugging (default off for security/performance)
; > QUICKSAVE
hotkey.quicksave = "Shift+F5"
hotkey.quickload = "Shift+F8"

View File

@ -545,6 +545,7 @@ function setup_all_libs ()
"boost",
"spidermonkey",
"valgrind",
"sdl",
}
setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {})

View File

@ -63,6 +63,7 @@ void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::strin
void* CMapGeneratorWorker::RunThread(void *data)
{
debug_SetThreadName("MapGenerator");
g_Profiler2.RegisterCurrentThread("MapGenerator");
CMapGeneratorWorker* self = static_cast<CMapGeneratorWorker*>(data);

View File

@ -75,6 +75,7 @@ JSBool JSI_IGUIObject::getProperty(JSContext* cx, JSObject* obj, jsid id, jsval*
if (propName == "constructor" ||
propName == "prototype" ||
propName == "toString" ||
propName == "toJSON" ||
propName == "focus" ||
propName == "blur" ||
propName == "getComputedSize"

View File

@ -209,6 +209,10 @@ namespace ConfigDB_JS
};
CConfigDB::CConfigDB()
{
}
void CConfigDB::RegisterJSConfigDB()
{
g_ScriptingHost.DefineCustomObjectType(&ConfigDB_JS::Class, ConfigDB_JS::Construct, 0, ConfigDB_JS::Props, ConfigDB_JS::Funcs, NULL, NULL);
g_ScriptingHost.DefineCustomObjectType(&ConfigNamespace_JS::Class, ConfigNamespace_JS::Construct, 0, NULL, ConfigNamespace_JS::Funcs, NULL, NULL);

View File

@ -80,9 +80,12 @@ class CConfigDB: public Singleton<CConfigDB>
static VfsPath m_ConfigFile[];
public:
// NOTE: Construct the Singleton Object *after* JavaScript init, so that
// the JS interface can be registered.
CConfigDB();
// NOTE: Construct the Singleton Object *after* JavaScript init, so that
// the JS interface can be registered. ConfigDB (C++) needs to be initialized before
// The ScriptInterface because the ScriptInterface requires some configuration information too.
void RegisterJSConfigDB();
/**
* Attempt to find a config variable with the given name; will search

View File

@ -21,6 +21,7 @@
#include "ps/ConfigDB.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "lib/timer.h"
#include "soundmanager/SoundManager.h"
@ -60,6 +61,9 @@ bool g_VSync = false;
bool g_Quickstart = false;
bool g_DisableAudio = false;
bool g_JSDebuggerEnabled = false;
bool g_ScriptProfilingEnabled = false;
// flag to switch on drawing terrain overlays
bool g_ShowPathfindingOverlay = false;
@ -127,6 +131,14 @@ static void LoadGlobals()
g_SoundManager->SetMemoryUsage(bufferSize, bufferCount);
}
#endif // CONFIG2_AUDIO
CFG_GET_VAL("jsdebugger.enable", Bool, g_JSDebuggerEnabled);
CFG_GET_VAL("profiler2.script.enable", Bool, g_ScriptProfilingEnabled);
// Script Debugging and profiling does not make sense together because of the hooks
// that reduce performance a lot - and it wasn't tested if it even works together.
if (g_JSDebuggerEnabled && g_ScriptProfilingEnabled)
LOGERROR(L"Enabling both script profiling and script debugging is not supported!");
}

View File

@ -82,6 +82,9 @@ extern bool g_VSync;
extern bool g_Quickstart;
extern bool g_DisableAudio;
extern bool g_JSDebuggerEnabled;
extern bool g_ScriptProfilingEnabled;
extern CStrW g_CursorName;
class CmdLineArgs;

View File

@ -84,6 +84,7 @@
#include "renderer/ModelRenderer.h"
#include "scripting/ScriptingHost.h"
#include "scripting/ScriptGlue.h"
#include "scriptinterface/DebuggingServer.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptStats.h"
#include "simulation2/Simulation2.h"
@ -692,6 +693,7 @@ void Shutdown(int UNUSED(flags))
TIMER_BEGIN(L"shutdown ScriptingHost");
delete &g_ScriptingHost;
delete g_DebuggingServer;
TIMER_END(L"shutdown ScriptingHost");
TIMER_BEGIN(L"shutdown ConfigDB");
@ -886,10 +888,16 @@ void Init(const CmdLineArgs& args, int UNUSED(flags))
CSoundManager::CreateSoundManager();
#endif
InitScripting(); // before GUI
// g_ConfigDB, command line args, globals
CONFIG_Init(args);
// before scripting
if (g_JSDebuggerEnabled)
g_DebuggingServer = new CDebuggingServer();
InitScripting(); // before GUI
g_ConfigDB.RegisterJSConfigDB(); // after scripting
// Optionally start profiler HTTP output automatically
// (By default it's only enabled by a hotkey, for security/performance)

View File

@ -0,0 +1,573 @@
/* 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/>.
*/
#include "precompiled.h"
#include "DebuggingServer.h"
#include "ThreadDebugger.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "scripting/JSConversions.h"
CDebuggingServer* g_DebuggingServer = NULL;
const char* CDebuggingServer::header400 =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
"Invalid request";
void CDebuggingServer::GetAllCallstacks(std::stringstream& response)
{
CScopeLock lock(m_Mutex);
response.str("");
std::stringstream stream;
uint nbrCallstacksWritten = 0;
std::list<CThreadDebugger*>::iterator itr;
if (m_ThreadDebuggers.size() > 0)
{
response << "[";
for (itr = m_ThreadDebuggers.begin(); itr != m_ThreadDebuggers.end(); itr++)
{
if ((*itr)->GetIsInBreak())
{
stream.str("");
std::string str = stream.str();
(*itr)->GetCallstack(stream);
str = stream.str();
if (stream.str() != "")
{
if (nbrCallstacksWritten != 0)
response << ",";
response << "{" << "\"ThreadDebuggerID\" : " << (*itr)->GetID() << ", \"CallStack\" : " << stream.str() << "}";
nbrCallstacksWritten++;
}
}
}
response << "]";
}
}
void CDebuggingServer::GetStackFrameData(std::stringstream& response, uint nestingLevel, uint threadDebuggerID, STACK_INFO stackInfoKind)
{
CScopeLock lock(m_Mutex);
response.str("");
std::stringstream stream;
std::list<CThreadDebugger*>::iterator itr;
for (itr = m_ThreadDebuggers.begin(); itr != m_ThreadDebuggers.end(); itr++)
{
if ( (*itr)->GetID() == threadDebuggerID && (*itr)->GetIsInBreak())
{
(*itr)->GetStackFrameData(stream, nestingLevel, stackInfoKind);
if (stream.str() != "")
{
response << stream.str();
}
}
}
}
CDebuggingServer::CDebuggingServer() :
m_MgContext(NULL)
{
m_BreakPointsSem = SDL_CreateSemaphore(0);
ENSURE(m_BreakPointsSem);
SDL_SemPost(m_BreakPointsSem);
m_LastThreadDebuggerID = 0; // Next will be 1, 0 is reserved
m_BreakRequestedByThread = false;
m_BreakRequestedByUser = false;
m_SettingSimultaneousThreadBreak = true;
m_SettingBreakOnException = true;
EnableHTTP();
LOGWARNING(L"Javascript debugging webserver enabled.");
}
CDebuggingServer::~CDebuggingServer()
{
SDL_DestroySemaphore(m_BreakPointsSem);
if (m_MgContext)
{
mg_stop(m_MgContext);
m_MgContext = NULL;
}
}
bool CDebuggingServer::SetNextDbgCmd(uint threadDebuggerID, DBGCMD dbgCmd)
{
CScopeLock lock(m_Mutex);
std::list<CThreadDebugger*>::iterator itr;
for (itr = m_ThreadDebuggers.begin(); itr != m_ThreadDebuggers.end(); itr++)
{
if ( (*itr)->GetID() == threadDebuggerID || threadDebuggerID == 0)
{
if (DBG_CMD_NONE == (*itr)->GetNextDbgCmd() && (*itr)->GetIsInBreak())
{
SetBreakRequestedByThread(false);
SetBreakRequestedByUser(false);
(*itr)->SetNextDbgCmd(dbgCmd);
}
}
}
return true;
}
void CDebuggingServer::SetBreakRequestedByThread(bool Enabled)
{
CScopeLock lock(m_Mutex1);
m_BreakRequestedByThread = Enabled;
}
bool CDebuggingServer::GetBreakRequestedByThread()
{
CScopeLock lock(m_Mutex1);
return m_BreakRequestedByThread;
}
void CDebuggingServer::SetBreakRequestedByUser(bool Enabled)
{
CScopeLock lock(m_Mutex1);
m_BreakRequestedByUser = Enabled;
}
bool CDebuggingServer::GetBreakRequestedByUser()
{
CScopeLock lock(m_Mutex1);
return m_BreakRequestedByUser;
}
void CDebuggingServer::SetSettingBreakOnException(bool Enabled)
{
CScopeLock lock(m_Mutex);
m_SettingBreakOnException = Enabled;
}
void CDebuggingServer::SetSettingSimultaneousThreadBreak(bool Enabled)
{
CScopeLock lock(m_Mutex1);
m_SettingSimultaneousThreadBreak = Enabled;
}
bool CDebuggingServer::GetSettingBreakOnException()
{
CScopeLock lock(m_Mutex);
return m_SettingBreakOnException;
}
bool CDebuggingServer::GetSettingSimultaneousThreadBreak()
{
CScopeLock lock(m_Mutex1);
return m_SettingSimultaneousThreadBreak;
}
static Status AddFileResponse(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData)
{
std::vector<std::string>& templates = *(std::vector<std::string>*)cbData;
std::wstring str(pathname.string());
templates.push_back(std::string(str.begin(), str.end()));
return INFO::OK;
}
void CDebuggingServer::EnumVfsJSFiles(std::stringstream& response)
{
VfsPath path = L"";
VfsPaths pathnames;
response.str("");
std::vector<std::string> templates;
vfs::ForEachFile(g_VFS, "", AddFileResponse, (uintptr_t)&templates, L"*.js", vfs::DIR_RECURSIVE);
std::vector<std::string>::iterator itr;
response << "[";
for (itr = templates.begin(); itr != templates.end(); itr++)
{
if (itr != templates.begin())
response << ",";
response << "\"" << *itr << "\"";
}
response << "]";
}
void CDebuggingServer::GetFile(std::string filename, std::stringstream& response)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
{
response << "Failed to load the file contents";
return;
}
std::string code = file.DecodeUTF8(); // assume it's UTF-8
response << code;
}
static void* MgDebuggingServerCallback_(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info)
{
CDebuggingServer* debuggingServer = (CDebuggingServer*)request_info->user_data;
ENSURE(debuggingServer);
return debuggingServer->MgDebuggingServerCallback(event, conn, request_info);
}
void* CDebuggingServer::MgDebuggingServerCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info)
{
void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling
const char* header200 =
"HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n" // TODO: not great for security
"Content-Type: text/plain; charset=utf-8\r\n\r\n";
const char* header404 =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
"Unrecognised URI";
switch (event)
{
case MG_NEW_REQUEST:
{
std::stringstream stream;
std::string uri = request_info->uri;
if (uri == "/GetThreadDebuggerStatus")
{
GetThreadDebuggerStatus(stream);
}
else if (uri == "/EnumVfsJSFiles")
{
EnumVfsJSFiles(stream);
}
else if (uri == "/GetAllCallstacks")
{
GetAllCallstacks(stream);
}
else if (uri == "/Continue")
{
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
return handled;
// TODO: handle the return value
SetNextDbgCmd(threadDebuggerID, DBG_CMD_CONTINUE);
}
else if (uri == "/Break")
{
SetBreakRequestedByUser(true);
}
else if (uri == "/SetSettingSimultaneousThreadBreak")
{
std::string strEnabled;
bool bEnabled = false;
if (!GetWebArgs(conn, request_info, "enabled", strEnabled))
return handled;
// TODO: handle the return value
if (strEnabled == "true")
bEnabled = true;
else if (strEnabled == "false")
bEnabled = false;
else
return handled; // TODO: return an error state
SetSettingSimultaneousThreadBreak(bEnabled);
}
else if (uri == "/GetSettingSimultaneousThreadBreak")
{
stream << "{ \"Enabled\" : " << (GetSettingSimultaneousThreadBreak() ? "true" : "false") << " } ";
}
else if (uri == "/SetSettingBreakOnException")
{
std::string strEnabled;
bool bEnabled = false;
if (!GetWebArgs(conn, request_info, "enabled", strEnabled))
return handled;
// TODO: handle the return value
if (strEnabled == "true")
bEnabled = true;
else if (strEnabled == "false")
bEnabled = false;
else
return handled; // TODO: return an error state
SetSettingBreakOnException(bEnabled);
}
else if (uri == "/GetSettingBreakOnException")
{
stream << "{ \"Enabled\" : " << (GetSettingBreakOnException() ? "true" : "false") << " } ";
}
else if (uri == "/Step")
{
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
return handled;
// TODO: handle the return value
SetNextDbgCmd(threadDebuggerID, DBG_CMD_SINGLESTEP);
}
else if (uri == "/StepInto")
{
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
return handled;
// TODO: handle the return value
SetNextDbgCmd(threadDebuggerID, DBG_CMD_STEPINTO);
}
else if (uri == "/StepOut")
{
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
return handled;
// TODO: handle the return value
SetNextDbgCmd(threadDebuggerID, DBG_CMD_STEPOUT);
}
else if (uri == "/GetStackFrame")
{
uint nestingLevel;
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "nestingLevel", nestingLevel) ||
!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
{
return handled;
}
GetStackFrameData(stream, nestingLevel, threadDebuggerID, STACK_INFO_LOCALS);
}
else if (uri == "/GetStackFrameThis")
{
uint nestingLevel;
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "nestingLevel", nestingLevel) ||
!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
{
return handled;
}
GetStackFrameData(stream, nestingLevel, threadDebuggerID, STACK_INFO_THIS);
}
else if (uri == "/GetCurrentGlobalObject")
{
uint threadDebuggerID;
if (!GetWebArgs(conn, request_info, "threadDebuggerID", threadDebuggerID))
{
return handled;
}
GetStackFrameData(stream, 0, threadDebuggerID, STACK_INFO_GLOBALOBJECT);
}
else if (uri == "/ToggleBreakpoint")
{
std::string filename;
uint line;
if (!GetWebArgs(conn, request_info, "filename", filename) ||
!GetWebArgs(conn, request_info, "line", line))
{
return handled;
}
ToggleBreakPoint(filename, line);
}
else if (uri == "/GetFile")
{
std::string filename;
if (!GetWebArgs(conn, request_info, "filename", filename))
return handled;
GetFile(filename, stream);
}
else
{
mg_printf(conn, "%s", header404);
return handled;
}
mg_printf(conn, "%s", header200);
std::string str = stream.str();
mg_write(conn, str.c_str(), str.length());
return handled;
}
case MG_HTTP_ERROR:
return NULL;
case MG_EVENT_LOG:
// Called by Mongoose's cry()
LOGERROR(L"Mongoose error: %hs", request_info->log_message);
return NULL;
case MG_INIT_SSL:
return NULL;
default:
debug_warn(L"Invalid Mongoose event type");
return NULL;
}
};
void CDebuggingServer::EnableHTTP()
{
// Ignore multiple enablings
if (m_MgContext)
return;
const char *options[] = {
"listening_ports", "127.0.0.1:9000", // bind to localhost for security
"num_threads", "6", // enough for the browser's parallel connection limit
NULL
};
m_MgContext = mg_start(MgDebuggingServerCallback_, this, options);
ENSURE(m_MgContext);
}
bool CDebuggingServer::GetWebArgs(struct mg_connection *conn, const struct mg_request_info* request_info, std::string argName, uint& arg)
{
if (!request_info->query_string)
{
mg_printf(conn, "%s (no query string)", header400);
return false;
}
char buf[256];
int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), argName.c_str(), buf, ARRAY_SIZE(buf));
if (len < 0)
{
mg_printf(conn, "%s (no '%s')", header400, argName.c_str());
return false;
}
arg = atoi(buf);
return true;
}
bool CDebuggingServer::GetWebArgs(struct mg_connection *conn, const struct mg_request_info* request_info, std::string argName, std::string& arg)
{
if (!request_info->query_string)
{
mg_printf(conn, "%s (no query string)", header400);
return false;
}
char buf[256];
int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), argName.c_str(), buf, ARRAY_SIZE(buf));
if (len < 0)
{
mg_printf(conn, "%s (no '%s')", header400, argName.c_str());
return false;
}
arg = buf;
return true;
}
void CDebuggingServer::RegisterScriptinterface(std::string name, ScriptInterface* pScriptInterface)
{
CScopeLock lock(m_Mutex);
CThreadDebugger* pThreadDebugger = new CThreadDebugger;
// ThreadID 0 is reserved
pThreadDebugger->Initialize(++m_LastThreadDebuggerID, name, pScriptInterface, this);
m_ThreadDebuggers.push_back(pThreadDebugger);
}
void CDebuggingServer::UnRegisterScriptinterface(ScriptInterface* pScriptInterface)
{
CScopeLock lock(m_Mutex);
std::list<CThreadDebugger*>::iterator itr;
for (itr = m_ThreadDebuggers.begin(); itr != m_ThreadDebuggers.end(); itr++)
{
if ((*itr)->CompareScriptInterfacePtr(pScriptInterface))
{
delete (*itr);
m_ThreadDebuggers.erase(itr);
break;
}
}
}
void CDebuggingServer::GetThreadDebuggerStatus(std::stringstream& response)
{
CScopeLock lock(m_Mutex);
response.str("");
std::list<CThreadDebugger*>::iterator itr;
response << "[";
for (itr = m_ThreadDebuggers.begin(); itr != m_ThreadDebuggers.end(); itr++)
{
if (itr == m_ThreadDebuggers.begin())
response << "{ ";
else
response << ",{ ";
response << "\"ThreadDebuggerID\" : " << (*itr)->GetID() << ",";
response << "\"ScriptInterfaceName\" : \"" << (*itr)->GetName() << "\",";
response << "\"ThreadInBreak\" : " << ((*itr)->GetIsInBreak() ? "true" : "false") << ",";
response << "\"BreakFileName\" : \"" << (*itr)->GetBreakFileName() << "\",";
response << "\"BreakLine\" : " << (*itr)->GetLastBreakLine();
response << " }";
}
response << "]";
}
void CDebuggingServer::ToggleBreakPoint(std::string filename, uint line)
{
// First, pass the message to all associated CThreadDebugger objects and check if one returns true (handled);
{
CScopeLock lock(m_Mutex);
std::list<CThreadDebugger*>::iterator itr;
for (itr = m_ThreadDebuggers.begin(); itr != m_ThreadDebuggers.end(); itr++)
{
if ((*itr)->ToggleBreakPoint(filename, line))
return;
}
}
// If the breakpoint isn't handled yet search the breakpoints registered in this class
std::list<CBreakPoint>* pBreakPoints = NULL;
double breakPointsLockID = AquireBreakPointAccess(&pBreakPoints);
std::list<CBreakPoint>::iterator itr;
// If set, delete
bool deleted = false;
for (itr = pBreakPoints->begin(); itr != pBreakPoints->end(); itr++)
{
if ((*itr).m_Filename == filename && (*itr).m_UserLine == line)
{
itr = pBreakPoints->erase(itr);
deleted = true;
break;
}
}
// If not set, set
if (!deleted)
{
CBreakPoint bP;
bP.m_Filename = filename;
bP.m_UserLine = line;
pBreakPoints->push_back(bP);
}
ReleaseBreakPointAccess(breakPointsLockID);
return;
}
double CDebuggingServer::AquireBreakPointAccess(std::list<CBreakPoint>** breakPoints)
{
int ret;
ret = SDL_SemWait(m_BreakPointsSem);
ENSURE(0 == ret);
(*breakPoints) = &m_BreakPoints;
m_BreakPointsLockID = timer_Time();
return m_BreakPointsLockID;
}
void CDebuggingServer::ReleaseBreakPointAccess(double breakPointsLockID)
{
ENSURE(m_BreakPointsLockID == breakPointsLockID);
SDL_SemPost(m_BreakPointsSem);
}

View File

@ -0,0 +1,151 @@
/* 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/>.
*/
#ifndef INCLUDED_DEBUGGINGSERVER
#define INCLUDED_DEBUGGINGSERVER
#include "third_party/mongoose/mongoose.h"
#include "ScriptInterface.h"
#include "lib/external_libraries/libsdl.h"
class CBreakPoint;
class CThreadDebugger;
enum DBGCMD { DBG_CMD_NONE=0, DBG_CMD_CONTINUE, DBG_CMD_SINGLESTEP, DBG_CMD_STEPINTO, DBG_CMD_STEPOUT };
enum STACK_INFO { STACK_INFO_LOCALS=0, STACK_INFO_THIS, STACK_INFO_GLOBALOBJECT };
class CDebuggingServer
{
public:
CDebuggingServer();
~CDebuggingServer();
/** @brief Register a new ScriptInerface for debugging the scripts it executes
*
* @param name A name for the ScriptInterface (will be sent to the debugging client an probably displayed to the user)
* @param pScriptInterface A pointer to the ScriptInterface. This pointer must stay valid until UnRegisterScriptInterface ist called!
*/
void RegisterScriptinterface(std::string name, ScriptInterface* pScriptInterface);
/** @brief Unregister a ScriptInerface that was previously registered using RegisterScriptinterface.
*
* @param pScriptInterface A pointer to the ScriptInterface
*/
void UnRegisterScriptinterface(ScriptInterface* pScriptInterface);
// Mongoose callback when request comes from a client
void* MgDebuggingServerCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info);
/** @brief Aquire exclusive read and write access to the list of breakpoints.
*
* @param breakPoints A pointer to the list storing all breakpoints.
*
* @return A number you need to pass to ReleaseBreakPointAccess().
*
* Make sure to call ReleaseBreakPointAccess after you don't need access any longer.
* Using this function you get exclusive access and other threads won't be able to access the breakpoints until you call ReleaseBreakPointAccess!
*/
double AquireBreakPointAccess(std::list<CBreakPoint>** breakPoints);
/** @brief See AquireBreakPointAccess(). You must not access the pointer returend by AquireBreakPointAccess() any longer after you call this function.
*
* @param breakPointsLockID The number you got when aquiring the access. It's used to make sure that this function never gets
* used by the wrong thread.
*/
void ReleaseBreakPointAccess(double breakPointsLockID);
/// Called from multiple Mongoose threads and multiple ScriptInterface threads
bool GetBreakRequestedByThread();
bool GetBreakRequestedByUser();
// Should other threads be stopped as soon as possible after a breakpoint is triggered in a thread
bool GetSettingSimultaneousThreadBreak();
// Should the debugger break on any JS-Exception? If set to false, it will only break when the exceptions text is "Breakpoint".
bool GetSettingBreakOnException();
void SetBreakRequestedByThread(bool Enabled);
void SetBreakRequestedByUser(bool Enabled);
private:
static const char* header400;
/// Webserver helper function (can be called by multiple mongooser threads)
bool GetWebArgs(struct mg_connection *conn, const struct mg_request_info* request_info, std::string argName, uint& arg);
bool GetWebArgs(struct mg_connection *conn, const struct mg_request_info* request_info, std::string argName, std::string& arg);
/// Functions that are made available via http (can be called by multiple mongoose threads)
void GetThreadDebuggerStatus(std::stringstream& response);
void ToggleBreakPoint(std::string filename, uint line);
void GetAllCallstacks(std::stringstream& response);
void GetStackFrameData(std::stringstream& response, uint nestingLevel, uint threadDebuggerID, STACK_INFO stackInfoKind);
bool SetNextDbgCmd(uint threadDebuggerID, DBGCMD dbgCmd);
void SetSettingSimultaneousThreadBreak(bool Enabled);
void SetSettingBreakOnException(bool Enabled);
/** @brief Returns a list of the full vfs paths to all files with the extension .js found in the vfs root
*
* @param response This will contain the list as JSON array.
*/
void EnumVfsJSFiles(std::stringstream& response);
/** @brief Get the content of a .js file loaded into vfs
*
* @param filename A full vfs path (as returned by EnumVfsJSFiles).
* @param response This will contain the contents of the requested file.
*/
void GetFile(std::string filename, std::stringstream& response);
/// Shared between multiple mongoose threads
bool m_SettingSimultaneousThreadBreak;
bool m_SettingBreakOnException;
/// Shared between multiple scriptinterface threads
uint m_LastThreadDebuggerID;
/// Shared between multiple scriptinerface threads and multiple mongoose threads
std::list<CThreadDebugger*> m_ThreadDebuggers;
// The CThreadDebuggers will check this value and break the thread if it's true.
// This only works for JS code, so C++ code will not break until it executes JS-code again.
bool m_BreakRequestedByThread;
bool m_BreakRequestedByUser;
// The breakpoint is uniquely identified using filename an line-number.
// Since the filename is the whole vfs path it should really be unique.
std::list<CBreakPoint> m_BreakPoints;
/// Used for controlling access to m_BreakPoints
SDL_sem* m_BreakPointsSem;
double m_BreakPointsLockID;
/// Mutexes used to ensure thread-safety. Currently we just use one Mutex (m_Mutex) and if we detect possible sources
/// of deadlocks, we use the second mutex for some members to avoid it.
CMutex m_Mutex;
CMutex m_Mutex1;
/// Not important for this class' thread-safety
void EnableHTTP();
mg_context* m_MgContext;
};
extern CDebuggingServer* g_DebuggingServer;
#endif // INCLUDED_DEBUGGINGSERVER

View File

@ -14,10 +14,10 @@
* 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/>.
*/
#include "ps/GameSetup/Config.h"
// (NativeWrapperDecls.h set up a lot of the macros we use here)
// ScriptInterface_NativeWrapper<T>::call(cx, rval, fptr, args...) will call fptr(cbdata, args...),
// and if T != void then it will store the result in rval:
@ -76,18 +76,17 @@ struct ScriptInterface_NativeMethodWrapper<void, TC> {
// ScriptInterface_impl::Register stores the name in a reserved slot.
// (TODO: this doesn't work for functions registered via InterfaceScripted.h.
// Maybe we should do some interned JS_GetFunctionId thing.)
#if ENABLE_SCRIPT_PROFILING
#define SCRIPT_PROFILE \
ENSURE(JSVAL_IS_OBJECT(JS_CALLEE(cx, vp)) && JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)))); \
const char* name = "(unknown)"; \
jsval nameval; \
if (JS_GetReservedSlot(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)), 0, &nameval) \
&& !JSVAL_IS_VOID(nameval)) \
name = static_cast<const char*>(JSVAL_TO_PRIVATE(nameval)); \
CProfileSampleScript profile(name);
#else
#define SCRIPT_PROFILE
#endif
if (g_ScriptProfilingEnabled) \
{ \
ENSURE(JSVAL_IS_OBJECT(JS_CALLEE(cx, vp)) && JS_ObjectIsFunction(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)))); \
const char* name = "(unknown)"; \
jsval nameval; \
if (JS_GetReservedSlot(cx, JSVAL_TO_OBJECT(JS_CALLEE(cx, vp)), 0, &nameval) \
&& !JSVAL_IS_VOID(nameval)) \
name = static_cast<const char*>(JSVAL_TO_PRIVATE(nameval)); \
CProfileSampleScript profile(name); \
}
// JSFastNative-compatible function that wraps the function identified in the template argument list
#define OVERLOADS(z, i, data) \

View File

@ -18,6 +18,7 @@
#include "precompiled.h"
#include "ScriptInterface.h"
#include "DebuggingServer.h"
#include "ScriptStats.h"
#include "AutoRooters.h"
@ -73,17 +74,18 @@ public:
m_rt = JS_NewRuntime(runtimeSize);
ENSURE(m_rt); // TODO: error handling
#if ENABLE_SCRIPT_PROFILING
// Profiler isn't thread-safe, so only enable this on the main thread
if (ThreadUtil::IsMainThread())
if (g_ScriptProfilingEnabled)
{
if (CProfileManager::IsInitialised())
// Profiler isn't thread-safe, so only enable this on the main thread
if (ThreadUtil::IsMainThread())
{
JS_SetExecuteHook(m_rt, jshook_script, this);
JS_SetCallHook(m_rt, jshook_function, this);
if (CProfileManager::IsInitialised())
{
JS_SetExecuteHook(m_rt, jshook_script, this);
JS_SetCallHook(m_rt, jshook_function, this);
}
}
}
#endif
JS_SetExtraGCRoots(m_rt, jshook_trace, this);
}
@ -100,7 +102,7 @@ public:
private:
#if ENABLE_SCRIPT_PROFILING
static void* jshook_script(JSContext* UNUSED(cx), JSStackFrame* UNUSED(fp), JSBool before, JSBool* UNUSED(ok), void* closure)
{
if (before)
@ -212,7 +214,6 @@ private:
return closure;
}
#endif
static void jshook_trace(JSTracer* trc, void* data)
{
@ -493,15 +494,17 @@ ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const sh
options |= JSOPTION_XML; // "ECMAScript for XML support: parse <!-- --> as a token"
options |= JSOPTION_VAROBJFIX; // "recommended" (fixes variable scoping)
// Enable method JIT, unless script profiling is enabled (since profiling
// Enable method JIT, unless script profiling/debugging is enabled (since profiling/debugging
// hooks are incompatible with the JIT)
#if !ENABLE_SCRIPT_PROFILING
options |= JSOPTION_METHODJIT;
// TODO: Verify what exactly is incompatible
if (!g_ScriptProfilingEnabled && !g_JSDebuggerEnabled)
{
options |= JSOPTION_METHODJIT;
// Some other JIT flags to experiment with:
options |= JSOPTION_JIT;
options |= JSOPTION_PROFILING;
#endif
// Some other JIT flags to experiment with:
options |= JSOPTION_JIT;
options |= JSOPTION_PROFILING;
}
JS_SetOptions(m_cx, options);
@ -557,20 +560,21 @@ void ScriptInterface_impl::Register(const char* name, JSNative fptr, uintN nargs
if (!func)
return;
#if ENABLE_SCRIPT_PROFILING
// Store the function name in a slot, so we can pass it to the profiler.
if (g_ScriptProfilingEnabled)
{
// Store the function name in a slot, so we can pass it to the profiler.
// Use a flyweight std::string because we can't assume the caller has
// a correctly-aligned string and that its lifetime is long enough
typedef boost::flyweight<
std::string,
boost::flyweights::no_tracking
// can't use no_locking; Register might be called in threads
> LockedStringFlyweight;
// Use a flyweight std::string because we can't assume the caller has
// a correctly-aligned string and that its lifetime is long enough
typedef boost::flyweight<
std::string,
boost::flyweights::no_tracking
// can't use no_locking; Register might be called in threads
> LockedStringFlyweight;
LockedStringFlyweight fw(name);
JS_SetReservedSlot(m_cx, JS_GetFunctionObject(func), 0, PRIVATE_TO_JSVAL((void*)fw.get().c_str()));
#endif
LockedStringFlyweight fw(name);
JS_SetReservedSlot(m_cx, JS_GetFunctionObject(func), 0, PRIVATE_TO_JSVAL((void*)fw.get().c_str()));
}
}
ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr<ScriptRuntime>& runtime) :
@ -582,6 +586,14 @@ ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugN
if (g_ScriptStatsTable)
g_ScriptStatsTable->Add(this, debugName);
}
if (g_JSDebuggerEnabled && g_DebuggingServer != NULL)
{
if(!JS_SetDebugMode(GetContext(), true))
LOGERROR(L"Failed to set Spidermonkey to debug mode!");
else
g_DebuggingServer->RegisterScriptinterface(debugName, this);
}
}
ScriptInterface::~ScriptInterface()
@ -591,6 +603,10 @@ ScriptInterface::~ScriptInterface()
if (g_ScriptStatsTable)
g_ScriptStatsTable->Remove(this);
}
// Unregister from the Debugger class
if (g_JSDebuggerEnabled && g_DebuggingServer != NULL)
g_DebuggingServer->UnRegisterScriptinterface(this);
}
void ScriptInterface::ShutDown()
@ -1075,12 +1091,14 @@ std::string ScriptInterface::StringifyJSON(jsval obj, bool indent)
{
JS_ClearPendingException(m->m_cx);
LOGERROR(L"StringifyJSON failed");
JS_ClearPendingException(m->m_cx);
return "";
}
return str.stream.str();
}
std::wstring ScriptInterface::ToString(jsval obj, bool pretty)
{
if (JSVAL_IS_VOID(obj))

View File

@ -43,17 +43,12 @@ class AutoGCRooter;
// TODO: what's a good default?
#define DEFAULT_RUNTIME_SIZE 16 * 1024 * 1024
#ifdef NDEBUG
#define ENABLE_SCRIPT_PROFILING 0
#else
#define ENABLE_SCRIPT_PROFILING 1
#endif
struct ScriptInterface_impl;
class ScriptRuntime;
class CDebuggingServer;
/**
* Abstraction around a SpiderMonkey JSContext.
*
@ -244,7 +239,7 @@ public:
* Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error.
*/
std::string StringifyJSON(jsval obj, bool indent = true);
/**
* Report the given error message through the JS error reporting mechanism,
* and throw a JS exception. (Callers can check IsPendingException, and must

View File

@ -0,0 +1,871 @@
/* 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/>.
*/
#include "precompiled.h"
#include "ThreadDebugger.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
// 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;
}
/// CThreadDebugger
void CThreadDebugger::ClearTrapsToRemove()
{
CScopeLock lock(m_Mutex);
std::list<CActiveBreakPoint*>::iterator itr=m_ActiveBreakPoints.begin();
while (itr != m_ActiveBreakPoints.end())
{
if ((*itr)->m_ToRemove)
{
ClearTrap((*itr));
// Remove the breakpoint
delete (*itr);
itr = 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_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_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_Mutex);
std::list<CActiveBreakPoint*>::iterator itr1;
for ( itr1 = m_ActiveBreakPoints.begin(); itr1 != 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_Mutex);
m_ActiveBreakPoints.push_back(pActiveBreakPoint);
}
itr = pBreakPoints->erase(itr);
continue;
}
}
itr++;
}
m_pDebuggingServer->ReleaseBreakPointAccess(breakPointsLockID);
}
bool CThreadDebugger::CheckIfMappingPresent(std::string filename, uint line)
{
bool isPresent = (m_LineToPCMap.end() != m_LineToPCMap.find(filename) && m_LineToPCMap[filename].end() != 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_LineToPCMap[filename][line].pBytecode;
JSScript* script = m_LineToPCMap[filename][line].pScript;
activeBreakPoint->m_Script = script;
activeBreakPoint->m_Pc = pc;
ENSURE(script != NULL && pc != NULL);
activeBreakPoint->m_ActualLine = JS_PCToLineNumber(m_pScriptInterface->GetContext(), script, pc);
JS_SetTrap(m_pScriptInterface->GetContext(), script, pc, TrapHandler_, PRIVATE_TO_JSVAL(this));
}
CThreadDebugger::CThreadDebugger()
{
m_NextDbgCmd = DBG_CMD_NONE;
m_IsInBreak = false;
m_pLastBreakFrame = new JSStackFrame*;
}
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_pScriptInterface->GetRuntime(), NULL, NULL);
JS_SetCallHook(m_pScriptInterface->GetRuntime(), NULL, NULL);
JS_SetNewScriptHook(m_pScriptInterface->GetRuntime(), NULL, NULL);
JS_SetDestroyScriptHook(m_pScriptInterface->GetRuntime(), NULL, NULL);
delete m_pLastBreakFrame;
}
void CThreadDebugger::ReturnActiveBreakPoints(jsbytecode* pBytecode)
{
CScopeLock lock(m_ActiveBreakpointsMutex);
std::list<CActiveBreakPoint*>::iterator itr;
itr = m_ActiveBreakPoints.begin();
while (itr != 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_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_ActiveBreakPoints.erase(itr);
m_pDebuggingServer->ReleaseBreakPointAccess(breakPointsLockID);
}
else
itr++;
}
}
void CThreadDebugger::Initialize(uint id, std::string name, ScriptInterface* pScriptInterface, CDebuggingServer* pDebuggingServer)
{
ENSURE(id != 0);
m_ID = id;
m_Name = name;
m_pScriptInterface = pScriptInterface;
m_pDebuggingServer = pDebuggingServer;
JS_SetExecuteHook(m_pScriptInterface->GetRuntime(), CallHook_, (void*)this);
JS_SetCallHook(m_pScriptInterface->GetRuntime(), CallHook_, (void*)this);
JS_SetNewScriptHook(m_pScriptInterface->GetRuntime(), NewScriptHook_, (void*)this);
JS_SetDestroyScriptHook(m_pScriptInterface->GetRuntime(), DestroyScriptHook_, (void*)this);
JS_SetThrowHook(m_pScriptInterface->GetRuntime(), ThrowHandler_, (void*)this);
if (m_pDebuggingServer->GetSettingSimultaneousThreadBreak())
{
// Setup a handler to check for break-requests from the DebuggingServer regularly
JS_SetInterrupt(m_pScriptInterface->GetRuntime(), 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_pScriptInterface->GetContext(), &iter);
uint lastBreakLine = GetLastBreakLine() ;
jsval val = JSVAL_VOID;
if ((*m_pLastBreakFrame == pStackFrame && lastBreakLine != line) ||
(*m_pLastBreakFrame != pStackFrame && !CurrentFrameIsChildOf(*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_pScriptInterface->GetContext(), &iter);
uint lastBreakLine = GetLastBreakLine();
jsval val = JSVAL_VOID;
if ((*m_pLastBreakFrame == pStackFrame && lastBreakLine != line) || *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_pScriptInterface->GetContext(), &iter);
if (pStackFrame != *m_pLastBreakFrame && !CurrentFrameIsChildOf(*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_pScriptInterface->GetContext(), &iter);
// Get the first parent Frame
fp = JS_FrameIterator(m_pScriptInterface->GetContext(), &iter);
while (fp)
{
if (fp == pParentFrame)
return true;
fp = JS_FrameIterator(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_pDebuggingServer->GetBreakRequestedByThread() || 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_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_pLastBreakFrame = NULL;
if (breakSrc == BREAK_SRC_INTERRUP)
{
JS_ClearInterrupt(m_pScriptInterface->GetRuntime(), NULL, NULL);
JS_SetSingleStepMode(cx, script, false);
}
if (m_pDebuggingServer->GetSettingSimultaneousThreadBreak())
{
m_pDebuggingServer->SetBreakRequestedByThread(true);
}
// Wait until the user continues the execution
while (1)
{
DBGCMD nextDbgCmd = GetNextDbgCmd();
while (!m_StackInfoRequests.empty())
{
StackInfoRequest request = m_StackInfoRequests.front();
SaveStackFrameData(request.requestType, request.nestingLevel);
SDL_SemPost(request.semaphore);
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_pLastBreakFrame = JS_FrameIterator(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_pScriptInterface->GetRuntime(), StepHandler_, this);
break;
}
else if (nextDbgCmd == DBG_CMD_STEPINTO)
{
JS_SetInterrupt(m_pScriptInterface->GetRuntime(), StepIntoHandler_, this);
break;
}
else if (nextDbgCmd == DBG_CMD_STEPOUT)
{
JS_SetInterrupt(m_pScriptInterface->GetRuntime(), 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_pScriptInterface->GetRuntime(), CheckForBreakRequestHandler_, this);
}
break;
}
else
debug_warn("Invalid DBGCMD found in CThreadDebugger::BreakHandler!");
}
ClearTrapsToRemove();
SetAllNewTraps();
SetNextDbgCmd(DBG_CMD_NONE);
SetIsInBreak(false);
SetBreakFileName("");
// All saved stack data becomes invalid
{
CScopeLock lock(m_Mutex);
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 == "")
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_LineToPCMap[stringFileName][line].firstLineInFunction;
lastLine = 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_LineToPCMap[stringFileName][line].pBytecode;
}
jsbytecode* pc = JS_LineNumberToPC (cx, script, line);
m_LineToPCMap[stringFileName][line].pBytecode = pc;
m_LineToPCMap[stringFileName][line].pScript = script;
m_LineToPCMap[stringFileName][line].firstLineInFunction = lineno;
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_LineToPCMap[fileName][line].pScript == script)
{
ReturnActiveBreakPoints(m_LineToPCMap[fileName][line].pBytecode);
m_LineToPCMap[fileName].erase(line);
if (m_LineToPCMap[fileName].empty())
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_Mutex);
std::list<CActiveBreakPoint*>::iterator itr;
for (itr = m_ActiveBreakPoints.begin(); itr != 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_Mutex);
response << m_Callstack;
}
void CThreadDebugger::SaveCallstack()
{
ENSURE(GetIsInBreak());
CScopeLock lock(m_Mutex);
JSStackFrame *fp;
JSStackFrame *iter = 0;
std::string functionName;
jsint counter = 0;
JSObject* jsArray;
jsArray = JS_NewArrayObject(m_pScriptInterface->GetContext(), 0, 0);
JSString* functionID;
fp = JS_FrameIterator(m_pScriptInterface->GetContext(), &iter);
while (fp)
{
JSFunction* fun = 0;
fun = JS_GetFrameFunction(m_pScriptInterface->GetContext(), fp);
if (NULL == fun)
functionID = JS_NewStringCopyZ(m_pScriptInterface->GetContext(), "null");
else
{
functionID = JS_GetFunctionId(fun);
if (NULL == functionID)
functionID = JS_NewStringCopyZ(m_pScriptInterface->GetContext(), "anonymous");
}
JSBool ret = JS_DefineElement(m_pScriptInterface->GetContext(), jsArray, counter, STRING_TO_JSVAL(functionID), NULL, NULL, 0);
ENSURE(ret);
fp = JS_FrameIterator(m_pScriptInterface->GetContext(), &iter);
counter++;
}
m_Callstack = "";
m_Callstack = 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_Mutex);
dataCached = (!m_StackFrameData.empty() && m_StackFrameData[stackInfoKind].end() != 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_Mutex);
{
response.str("");
response << 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_StackInfoRequests.push(request);
}
void CThreadDebugger::SaveStackFrameData(STACK_INFO stackInfo, uint nestingLevel)
{
ENSURE(GetIsInBreak());
CScopeLock lock(m_Mutex);
JSStackFrame *fp;
JSStackFrame *iter = 0;
uint counter = 0;
jsval val;
if (stackInfo == STACK_INFO_GLOBALOBJECT)
{
JSObject* obj;
obj = JS_GetGlobalForScopeChain(m_pScriptInterface->GetContext());
m_StackFrameData[stackInfo][nestingLevel] = StringifyCyclicJSON(OBJECT_TO_JSVAL(obj), false);
}
else
{
fp = JS_FrameIterator(m_pScriptInterface->GetContext(), &iter);
while (fp)
{
if (counter == nestingLevel)
{
if (stackInfo == STACK_INFO_LOCALS)
{
JSObject* obj;
obj = JS_GetFrameCallObject(m_pScriptInterface->GetContext(), fp);
//obj = JS_GetFrameScopeChain(m_pScriptInterface->GetContext(), fp);
m_StackFrameData[stackInfo][nestingLevel] = StringifyCyclicJSON(OBJECT_TO_JSVAL(obj), false);
}
else if (stackInfo == STACK_INFO_THIS)
{
if (JS_GetFrameThis(m_pScriptInterface->GetContext(), fp, &val))
{
m_StackFrameData[stackInfo][nestingLevel] = StringifyCyclicJSON(val, false);
}
else
m_StackFrameData[stackInfo][nestingLevel] = "";
}
}
counter++;
fp = JS_FrameIterator(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 globabl 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_pScriptInterface->GetGlobalObject());
JSFunction* fun = JS_DefineFunction(m_pScriptInterface->GetContext(), pGlob, "replacer", CyclicRefWorkaround::replacer, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
JSObject* replacer = JS_GetFunctionObject(fun);
if (!JS_Stringify(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_pScriptInterface->GetContext(), &exec))
{
if (JSVAL_IS_OBJECT(exec))
{
JS_GetProperty(m_pScriptInterface->GetContext(), JSVAL_TO_OBJECT(exec), "message", &execString);
if (JSVAL_IS_STRING(execString))
{
std::string strExec = JS_EncodeString(m_pScriptInterface->GetContext(), JSVAL_TO_STRING(execString));
LOGERROR(L"Error: %hs", strExec.c_str());
}
}
}
JS_ClearPendingException(m_pScriptInterface->GetContext());
return "";
}
return str.stream.str();
}
bool CThreadDebugger::CompareScriptInterfacePtr(ScriptInterface* pScriptInterface)
{
return (pScriptInterface == m_pScriptInterface);
}
std::string CThreadDebugger::GetBreakFileName()
{
CScopeLock lock(m_Mutex);
return m_BreakFileName;
}
void CThreadDebugger::SetBreakFileName(std::string breakFileName)
{
CScopeLock lock(m_Mutex);
m_BreakFileName = breakFileName;
}
uint CThreadDebugger::GetLastBreakLine()
{
CScopeLock lock(m_Mutex);
return m_LastBreakLine;
}
void CThreadDebugger::SetLastBreakLine(uint breakLine)
{
CScopeLock lock(m_Mutex);
m_LastBreakLine = breakLine;
}
bool CThreadDebugger::GetIsInBreak()
{
CScopeLock lock(m_IsInBreakMutex);
return m_IsInBreak;
}
void CThreadDebugger::SetIsInBreak(bool isInBreak)
{
CScopeLock lock(m_IsInBreakMutex);
m_IsInBreak = isInBreak;
}
void CThreadDebugger::SetNextDbgCmd(DBGCMD dbgCmd)
{
CScopeLock lock(m_NextDbgCmdMutex);
m_NextDbgCmd = dbgCmd;
}
DBGCMD CThreadDebugger::GetNextDbgCmd()
{
CScopeLock lock(m_NextDbgCmdMutex);
return m_NextDbgCmd;
}
std::string CThreadDebugger::GetName()
{
CScopeLock lock(m_Mutex);
return m_Name;
}
uint CThreadDebugger::GetID()
{
CScopeLock lock(m_Mutex);
return m_ID;
}

View File

@ -0,0 +1,233 @@
/* 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/>.
*/
#ifndef INCLUDED_THREADDEBUGGER
#define INCLUDED_THREADDEBUGGER
#include "DebuggingServer.h"
#include "ScriptInterface.h"
#include "scriptinterface/ScriptExtraHeaders.h"
// These Breakpoint classes are not implemented threadsafe. The class using the Breakpoints is responsible to make sure that
// only one thread accesses the Breakpoint at a time
class CBreakPoint
{
public:
CBreakPoint() { m_UserLine = 0; m_Filename = ""; }
uint m_UserLine;
std::string m_Filename;
};
// Only use this with one ScriptInterface/CThreadDebugger!
class CActiveBreakPoint : public CBreakPoint
{
public:
CActiveBreakPoint() :
CBreakPoint()
{
Initialize();
}
CActiveBreakPoint(CBreakPoint breakPoint)
{
m_Filename = breakPoint.m_Filename;
m_UserLine = breakPoint.m_UserLine;
Initialize();
}
void Initialize()
{
m_ToRemove = false;
m_Script = NULL;
m_Pc = NULL;
m_ActualLine = m_UserLine;
}
uint m_ActualLine;
JSScript* m_Script;
jsbytecode* m_Pc;
bool m_ToRemove;
};
enum BREAK_SRC { BREAK_SRC_TRAP, BREAK_SRC_INTERRUP, BREAK_SRC_EXCEPTION };
class CThreadDebugger
{
public:
CThreadDebugger();
~CThreadDebugger();
/** @brief Initialize the object (required before using the object!).
*
* @param id A unique identifier greater than 0 for the object inside its CDebuggingServer object.
* @param name A name that will be can be displayed by the UI to identify the thread.
* @param pScriptInterface Pointer to a scriptinterface. All Hooks, breakpoint traps etc. will be registered in this
* scriptinterface and will be called by the thread this scriptinterface is running in.
* @param pDebuggingServer Pointer to the DebuggingServer object this Object should belong to.
*
* @return Return value.
*/
void Initialize(uint id, std::string name, ScriptInterface* pScriptInterface, CDebuggingServer* pDebuggingServer);
// A bunch of hooks used to get information from spidermonkey.
// These hooks are used internally only but have to be public because they need to be accessible from the global hook functions.
// Spidermonkey requires function pointers as hooks, which only works if the functions are global or static (not part of an object).
// These global functions in ThreadDebugger.cpp are just wrappers for the following member functions.
/** Simply calls BreakHandler with BREAK_SRC_TRAP */
JSTrapStatus TrapHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, jsval closure);
/** Hook to capture exceptions and breakpoints in code (throw "Breakpoint";) */
JSTrapStatus ThrowHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval);
/** All other hooks call this one if the execution should be paused. It puts the program in a wait-loop and
* waits for weak-up events (continue, step etc.). */
JSTrapStatus BreakHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, jsval closure, BREAK_SRC breakSrc);
JSTrapStatus StepHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void *closure);
JSTrapStatus StepIntoHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void *closure);
JSTrapStatus StepOutHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void *closure);
/** This is an interrup-hook that can be called multiple times per line of code and is used to break into the execution
* without previously setting a breakpoint and to break other threads when one thread triggers a breakpoint */
JSTrapStatus CheckForBreakRequestHandler(JSContext *cx, JSScript *script, jsbytecode *pc, jsval *rval, void *closure);
/** The callback function which gets executed for each new script that gets loaded and each function inside a script.
* We use it for "Execute-Hooks" and "Call-Hooks" in terms of Spidermonkey.
* This hook actually sets the traps (Breakpoints) "on the fly" that have been defined by the user previously.
*/
void ExecuteHook(JSContext *cx, const char *filename, unsigned lineno, JSScript *script, JSFunction *fun, void *callerdata);
/** This hook is used to update the mapping between filename plus line-numbers and jsbytecode pointers */
void NewScriptHook(JSContext *cx, const char *filename, unsigned lineno, JSScript *script, JSFunction *fun, void *callerdata);
/** This hook makes sure that invalid mappings between filename plus line-number and jsbytecode points get deleted */
void DestroyScriptHook(JSContext *cx, JSScript *script);
void ClearTrap(CActiveBreakPoint* activeBreakPoint);
/** @brief Checks if a mapping for the specified filename and line number exists in this CThreadDebugger's context
*/
bool CheckIfMappingPresent(std::string filename, uint line);
/** @brief Checks if a mapping exists for each breakpoint in the list of breakpoints that aren't set yet.
* If there is a mapping, it removes the breakpoint from the list of unset breakpoints (from CDebuggingServer),
* adds it to the list of active breakpoints (CThreadDebugger) and sets a trap.
* Threading: m_Mutex is locked in this call
*/
void SetAllNewTraps();
/** @brief Sets a new trap and stores the information in the CActiveBreakPoint pointer
* Make sure that a mapping exists before calling this function
* Threading: Locking m_Mutex is required by the callee
*/
void SetNewTrap(CActiveBreakPoint* activeBreakPoint, std::string filename, uint line);
/** @brief Toggle a breakpoint if it's active in this threadDebugger object.
* Threading: Locking m_Mutex is required by the callee
*
* @param filename full vfs path to the script filename
* @param userLine linenumber where the USER set the breakpoint (UserLine)
*
* @return true if the breakpoint's state was changed
*/
bool ToggleBreakPoint(std::string filename, uint userLine);
void GetCallstack(std::stringstream& response);
void GetStackFrameData(std::stringstream& response, uint nestingLevel, STACK_INFO stackInfoKind);
/** @brief Compares the object's associated scriptinterface with the pointer passed as parameter.
* @return true if equal
*/
bool CompareScriptInterfacePtr(ScriptInterface* pScriptInterface);
// Getter/Setters for members that need to be threadsafe
std::string GetBreakFileName();
bool GetIsInBreak();
uint GetLastBreakLine();
std::string GetName();
uint GetID();
void ContinueExecution();
void SetNextDbgCmd(DBGCMD dbgCmd);
DBGCMD GetNextDbgCmd();
// The callee is responsible for locking m_Mutex
void AddStackInfoRequest(STACK_INFO requestType, uint nestingLevel, SDL_sem* semaphore);
private:
// Getters/Setters for members that need to be threadsafe
void SetBreakFileName(std::string breakFileName);
void SetLastBreakLine(uint breakLine);
void SetIsInBreak(bool isInBreak);
// Other threadsafe functions
void SaveCallstack();
CMutex m_Mutex;
CMutex m_ActiveBreakpointsMutex;
CMutex m_NextDbgCmdMutex;
CMutex m_IsInBreakMutex;
/// Used only in the scriptinterface's thread.
void ClearTrapsToRemove();
bool CurrentFrameIsChildOf(JSStackFrame* pParentFrame);
void ReturnActiveBreakPoints(jsbytecode* pBytecode);
void SaveStackFrameData(STACK_INFO stackInfo, uint nestingLevel);
std::string StringifyCyclicJSON(jsval obj, bool indent);
// 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;
struct StackInfoRequest
{
STACK_INFO requestType;
uint nestingLevel;
SDL_sem* semaphore;
};
std::queue<StackInfoRequest> m_StackInfoRequests;
struct trapLocation
{
jsbytecode* pBytecode;
JSScript* pScript;
uint firstLineInFunction;
uint lastLineInFunction;
};
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;
};
#endif // INCLUDED_THREADDEBUGGER