diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 4dc1717ac6..9c472e1a6b 100644 --- a/binaries/data/config/default.cfg +++ b/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" diff --git a/build/premake/premake4.lua b/build/premake/premake4.lua index 403ddd9744..bbf673e473 100644 --- a/build/premake/premake4.lua +++ b/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, {}) diff --git a/source/graphics/MapGenerator.cpp b/source/graphics/MapGenerator.cpp index b804165dae..fe6ec02555 100644 --- a/source/graphics/MapGenerator.cpp +++ b/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(data); diff --git a/source/gui/scripting/JSInterface_IGUIObject.cpp b/source/gui/scripting/JSInterface_IGUIObject.cpp index a38c65477f..6b7c6b58e6 100644 --- a/source/gui/scripting/JSInterface_IGUIObject.cpp +++ b/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" diff --git a/source/ps/ConfigDB.cpp b/source/ps/ConfigDB.cpp index d429eba64c..3af2521e06 100644 --- a/source/ps/ConfigDB.cpp +++ b/source/ps/ConfigDB.cpp @@ -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); diff --git a/source/ps/ConfigDB.h b/source/ps/ConfigDB.h index 6d7cd8c4b6..8f1e039551 100644 --- a/source/ps/ConfigDB.h +++ b/source/ps/ConfigDB.h @@ -80,9 +80,12 @@ class CConfigDB: public Singleton 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 diff --git a/source/ps/GameSetup/Config.cpp b/source/ps/GameSetup/Config.cpp index 6d520db715..13ddcb041e 100644 --- a/source/ps/GameSetup/Config.cpp +++ b/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!"); } diff --git a/source/ps/GameSetup/Config.h b/source/ps/GameSetup/Config.h index c1973f0040..9dc657e507 100644 --- a/source/ps/GameSetup/Config.h +++ b/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; diff --git a/source/ps/GameSetup/GameSetup.cpp b/source/ps/GameSetup/GameSetup.cpp index fcf883a31f..4c9aeb466d 100644 --- a/source/ps/GameSetup/GameSetup.cpp +++ b/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) diff --git a/source/scriptinterface/DebuggingServer.cpp b/source/scriptinterface/DebuggingServer.cpp new file mode 100644 index 0000000000..7a54926ef2 --- /dev/null +++ b/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 . + */ + +#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::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::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::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& templates = *(std::vector*)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 templates; + vfs::ForEachFile(g_VFS, "", AddFileResponse, (uintptr_t)&templates, L"*.js", vfs::DIR_RECURSIVE); + + std::vector::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::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::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::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* pBreakPoints = NULL; + double breakPointsLockID = AquireBreakPointAccess(&pBreakPoints); + std::list::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** 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); +} diff --git a/source/scriptinterface/DebuggingServer.h b/source/scriptinterface/DebuggingServer.h new file mode 100644 index 0000000000..a62c331dc0 --- /dev/null +++ b/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 . + */ + +#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** 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 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 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 diff --git a/source/scriptinterface/NativeWrapperDefns.h b/source/scriptinterface/NativeWrapperDefns.h index f54cb01bca..6704a8738b 100644 --- a/source/scriptinterface/NativeWrapperDefns.h +++ b/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 . */ + #include "ps/GameSetup/Config.h" // (NativeWrapperDecls.h set up a lot of the macros we use here) - // ScriptInterface_NativeWrapper::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 { // 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(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(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) \ diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index 2276b73aa0..4c5cf3533f 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/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 @@ 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& 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)) diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h index f80f671710..f97981e7a6 100644 --- a/source/scriptinterface/ScriptInterface.h +++ b/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 @@ 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 diff --git a/source/scriptinterface/ThreadDebugger.cpp b/source/scriptinterface/ThreadDebugger.cpp new file mode 100644 index 0000000000..e7c0fd71d8 --- /dev/null +++ b/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 . + */ + +#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::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* pBreakPoints = NULL; + double breakPointsLockID; + breakPointsLockID = m_pDebuggingServer->AquireBreakPointAccess(&pBreakPoints); + std::list::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::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::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* 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::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 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(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; +} + diff --git a/source/scriptinterface/ThreadDebugger.h b/source/scriptinterface/ThreadDebugger.h new file mode 100644 index 0000000000..d550a5b5d1 --- /dev/null +++ b/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 . + */ + +#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 m_StackInfoRequests; + + struct trapLocation + { + jsbytecode* pBytecode; + JSScript* pScript; + uint firstLineInFunction; + uint lastLineInFunction; + }; + std::map > m_LineToPCMap; + std::list m_ActiveBreakPoints; + std::map > 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