Adds the server-side part of the javascript debugger. Refs #410
This was SVN commit r13238.
This commit is contained in:
parent
f5be596ee8
commit
73951b75fc
@ -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"
|
||||
|
@ -545,6 +545,7 @@ function setup_all_libs ()
|
||||
"boost",
|
||||
"spidermonkey",
|
||||
"valgrind",
|
||||
"sdl",
|
||||
}
|
||||
setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {})
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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!");
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
573
source/scriptinterface/DebuggingServer.cpp
Normal file
573
source/scriptinterface/DebuggingServer.cpp
Normal 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);
|
||||
}
|
151
source/scriptinterface/DebuggingServer.h
Normal file
151
source/scriptinterface/DebuggingServer.h
Normal 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
|
@ -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) \
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
871
source/scriptinterface/ThreadDebugger.cpp
Normal file
871
source/scriptinterface/ThreadDebugger.cpp
Normal 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;
|
||||
}
|
||||
|
233
source/scriptinterface/ThreadDebugger.h
Normal file
233
source/scriptinterface/ThreadDebugger.h
Normal 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
|
Loading…
Reference in New Issue
Block a user