forked from 0ad/0ad
574 lines
15 KiB
C++
574 lines
15 KiB
C++
/* Copyright (C) 2013 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#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);
|
|
}
|