Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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

git-svn-id: http://svn.wildfiregames.com/public/ps/trunk@13238 3db68df2-c116-0410-a063-a993310a9797
  • Loading branch information...
commit 3ba51149b4d76ec14e6bc360d4f1dc4bc5049ac8 1 parent 58eeab6
Yves authored
View
4 binaries/data/config/default.cfg
@@ -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
1  build/premake/premake4.lua
@@ -545,6 +545,7 @@ function setup_all_libs ()
"boost",
"spidermonkey",
"valgrind",
+ "sdl",
}
setup_static_lib_project("scriptinterface", source_dirs, extern_libs, {})
View
1  source/graphics/MapGenerator.cpp
@@ -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
1  source/gui/scripting/JSInterface_IGUIObject.cpp
@@ -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
4 source/ps/ConfigDB.cpp
@@ -210,6 +210,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);
JSObject *js_ConfigDB = g_ScriptingHost.CreateCustomObject("ConfigDB");
View
7 source/ps/ConfigDB.h
@@ -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
12 source/ps/GameSetup/Config.cpp
@@ -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
3  source/ps/GameSetup/Config.h
@@ -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
12 source/ps/GameSetup/GameSetup.cpp
@@ -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
573 source/scriptinterface/DebuggingServer.cpp
@@ -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
151 source/scriptinterface/DebuggingServer.h
@@ -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
23 source/scriptinterface/NativeWrapperDefns.h
@@ -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
74 source/scriptinterface/ScriptInterface.cpp
@@ -18,6 +18,7 @@
#include "precompiled.h"
#include "ScriptInterface.h"
+#include "DebuggingServer.h"
#include "ScriptStats.h"
#include "AutoRooters.h"
@@ -73,17 +74,18 @@ class ScriptRuntime
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 @@ class ScriptRuntime
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 @@ class ScriptRuntime
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
11 source/scriptinterface/ScriptInterface.h
@@ -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 @@ class ScriptInterface
* 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
871 source/scriptinterface/ThreadDebugger.cpp
@@ -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
233 source/scriptinterface/ThreadDebugger.h
@@ -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
Please sign in to comment.
Something went wrong with that request. Please try again.