mirror of
https://github.com/copyrighttxt/watrbx-game-engine.git
synced 2026-09-04 20:57:49 +00:00
2202 lines
64 KiB
C++
2202 lines
64 KiB
C++
#include "stdafx.h"
|
|
#include "script/DebuggerManager.h"
|
|
|
|
#include "script/ScriptContext.h"
|
|
#include "script/Script.h"
|
|
#include "script/ModuleScript.h"
|
|
#include "script/LuaArguments.h"
|
|
#include "util/ProtectedString.h"
|
|
|
|
#include "lua/lua.hpp"
|
|
#include "lstate.h"
|
|
#include "ltable.h"
|
|
|
|
#include "v8datamodel/DataModel.h"
|
|
#include "boost/tokenizer.hpp"
|
|
#include "boost/algorithm/string.hpp"
|
|
|
|
FASTFLAGVARIABLE(LuaDebugger, false)
|
|
FASTFLAGVARIABLE(LuaDebuggerBreakOnError, false)
|
|
|
|
namespace RBX
|
|
{
|
|
namespace Scripting
|
|
{
|
|
const char* const sDebuggerManager = "DebuggerManager";
|
|
const char* const sScriptDebugger = "ScriptDebugger";
|
|
const char* const sDebuggerBreakpoint = "DebuggerBreakpoint";
|
|
const char* const sDebuggerWatch = "DebuggerWatch";
|
|
|
|
static void checkDebugging()
|
|
{
|
|
if (!FFlag::LuaDebugger)
|
|
throw std::runtime_error("debugging is disabled");
|
|
}
|
|
|
|
class PauseNowBreakpoint : public ISpecialBreakpoint
|
|
{
|
|
public:
|
|
PauseNowBreakpoint()
|
|
{
|
|
baseThread = NULL;
|
|
}
|
|
|
|
virtual bool hitTest(lua_State* L, lua_Debug *ar)
|
|
{
|
|
return ar->event == LUA_HOOKLINE;
|
|
}
|
|
};
|
|
|
|
class StepInBreakpoint : public ISpecialBreakpoint
|
|
{
|
|
bool hitPausedLine;
|
|
public:
|
|
StepInBreakpoint():hitPausedLine(false)
|
|
{
|
|
baseThread = NULL;
|
|
}
|
|
virtual bool hitTest(lua_State* L, lua_Debug *ar)
|
|
{
|
|
if (baseThread && (baseThread != L))
|
|
return false;
|
|
|
|
if (!hitPausedLine)
|
|
{
|
|
hitPausedLine = ar->event == LUA_HOOKLINE;
|
|
return false;
|
|
}
|
|
|
|
// The next line we hit is either a step-in or a step-over. Either is fine
|
|
if (ar->event == LUA_HOOKLINE)
|
|
return true;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
class StepOutBreakpoint : public ISpecialBreakpoint
|
|
{
|
|
int depth;
|
|
bool readyForLine;
|
|
public:
|
|
StepOutBreakpoint():depth(0),readyForLine(false)
|
|
{
|
|
baseThread = NULL;
|
|
}
|
|
|
|
virtual bool hitTest(lua_State* L, lua_Debug *ar)
|
|
{
|
|
if (baseThread && (baseThread != L))
|
|
return false;
|
|
|
|
if (readyForLine)
|
|
return ar->event == LUA_HOOKLINE;
|
|
|
|
RBXASSERT(depth >= 0);
|
|
|
|
if (strcmp(ar->what, "C") != 0)
|
|
{
|
|
if (ar->event == LUA_HOOKRET) // what about LUA_HOOKTAILRET?
|
|
depth--;
|
|
else if (ar->event == LUA_HOOKCALL)
|
|
depth++;
|
|
}
|
|
|
|
RBXASSERT(depth >= -1);
|
|
readyForLine = depth == -1;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
class StepOverBreakpoint : public ISpecialBreakpoint
|
|
{
|
|
int depth;
|
|
bool hitPausedLine;
|
|
public:
|
|
StepOverBreakpoint():depth(0),hitPausedLine(false)
|
|
{
|
|
baseThread = NULL;
|
|
}
|
|
|
|
virtual bool hitTest(lua_State* L, lua_Debug *ar)
|
|
{
|
|
if (baseThread && (baseThread != L))
|
|
return false;
|
|
|
|
if (!hitPausedLine)
|
|
{
|
|
hitPausedLine = ar->event == LUA_HOOKLINE;
|
|
return false;
|
|
}
|
|
|
|
if (depth == 0 && ar->event == LUA_HOOKLINE)
|
|
return true;
|
|
|
|
if (depth < 0)
|
|
// We stepped out, so all we are waiting for is the next line
|
|
return ar->event == LUA_HOOKLINE;
|
|
|
|
RBXASSERT(depth >= 0);
|
|
|
|
//if (strcmp(ar->what, "C") != 0)
|
|
{
|
|
if (ar->event == LUA_HOOKRET) // what about LUA_HOOKTAILRET?
|
|
depth--;
|
|
else if (ar->event == LUA_HOOKCALL)
|
|
depth++;
|
|
}
|
|
|
|
RBXASSERT(depth >= -1);
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
using namespace RBX;
|
|
using namespace Scripting;
|
|
|
|
REFLECTION_BEGIN();
|
|
static Reflection::BoundFuncDesc<DebuggerManager, shared_ptr<Instance>(shared_ptr<Instance>)> func_AddDebugger(&DebuggerManager::addDebugger_Reflection, "AddDebugger", "script", Security::None);
|
|
static Reflection::BoundFuncDesc<DebuggerManager, void()> func_Enable(&DebuggerManager::enableDebugging, "EnableDebugging", Security::LocalUser);
|
|
static Reflection::PropDescriptor<DebuggerManager, bool> prop_Enabled("DebuggingEnabled", category_Data, &DebuggerManager::getEnabled, NULL);
|
|
static Reflection::BoundFuncDesc<DebuggerManager, void()> func_Resume(&DebuggerManager::resume, "Resume", Security::None);
|
|
static Reflection::BoundFuncDesc<DebuggerManager, void()> func_StepOver(&DebuggerManager::stepOver, "StepOver", Security::None);
|
|
static Reflection::BoundFuncDesc<DebuggerManager, void()> func_StepInto(&DebuggerManager::stepInto, "StepIn", Security::None);
|
|
static Reflection::BoundFuncDesc<DebuggerManager, void()> func_StepOut(&DebuggerManager::stepOut, "StepOut", Security::None);
|
|
static Reflection::BoundFuncDesc<DebuggerManager, shared_ptr<const Instances>()> func_GetDebuggers(&DebuggerManager::getDebuggers_Reflection, "GetDebuggers", Security::None);
|
|
static Reflection::EventDesc<DebuggerManager, void(shared_ptr<Instance>)> event_DebuggerAdded(&DebuggerManager::debuggerAdded, "DebuggerAdded", "debugger");
|
|
static Reflection::EventDesc<DebuggerManager, void(shared_ptr<Instance>)> event_DebuggerRemoved(&DebuggerManager::debuggerRemoved, "DebuggerRemoved", "debugger");
|
|
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<Instance>(int)> func_SetBreakpoint(&ScriptDebugger::setBreakpoint_Reflection, "SetBreakpoint", "line", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<const Instances>()> func_GetBreakpoints(&ScriptDebugger::getBreakpoints_Reflection, "GetBreakpoints", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<Instance>(std::string)> func_AddWatch(&ScriptDebugger::addWatch_Reflection, "AddWatch", "expression", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<const Instances>()> func_GetWatches(&ScriptDebugger::getWatches_Reflection, "GetWatches", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, Reflection::Variant(shared_ptr<Instance>)> func_GetValue(&ScriptDebugger::getWatchValue_Reflection, "GetWatchValue", "watch", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void()> func_Resume_dep(&ScriptDebugger::resume, "Resume", Security::None, Reflection::Descriptor::Attributes::deprecated());
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void()> func_StepOver_dep(&ScriptDebugger::stepOver, "StepOver", Security::None, Reflection::Descriptor::Attributes::deprecated());
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void()> func_StepInto_dep(&ScriptDebugger::stepInto, "StepIn", Security::None, Reflection::Descriptor::Attributes::deprecated());
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void()> func_StepOut_dep(&ScriptDebugger::stepOut, "StepOut", Security::None, Reflection::Descriptor::Attributes::deprecated());
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<const Reflection::ValueArray>()> func_GetStack(&ScriptDebugger::getStack_Reflection, "GetStack", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<const Reflection::ValueMap>(int)> func_GetLocals(&ScriptDebugger::getLocals, "GetLocals", "stackFrame", 0, Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<const Reflection::ValueMap>(int)> func_GetUpvalues(&ScriptDebugger::getUpvalues, "GetUpvalues", "stackFrame", 0, Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, shared_ptr<const Reflection::ValueMap>()> func_GetGlobals(&ScriptDebugger::getGlobals, "GetGlobals", Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void(std::string, Reflection::Variant, int)> func_SetLocal(&ScriptDebugger::setLocal, "SetLocal", "name", "value", "stackFrame", 0, Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void(std::string, Reflection::Variant, int)> func_SetUpvalue(&ScriptDebugger::setUpvalue, "SetUpvalue", "name", "value", "stackFrame", 0, Security::None);
|
|
static Reflection::BoundFuncDesc<ScriptDebugger, void(std::string, Reflection::Variant)> func_SetGlobal(&ScriptDebugger::setGlobal, "SetGlobal", "name", "value", Security::None);
|
|
// The Script property is read-only by design. If we want to make it writable, then we need to watch for changes and update DebuggerManager::debuggers
|
|
const Reflection::RefPropDescriptor<ScriptDebugger, Instance> prop_Script("Script", category_Data, &ScriptDebugger::getScript, NULL, Reflection::PropertyDescriptor::UI);
|
|
const Reflection::PropDescriptor<ScriptDebugger, std::string> prop_ScriptPath("ScriptPath", category_Data, &ScriptDebugger::getScriptPath, &ScriptDebugger::setScriptPath, Reflection::PropertyDescriptor::STREAMING);
|
|
static Reflection::PropDescriptor<ScriptDebugger, bool> prop_IsDebugging("IsDebugging", category_Data, &ScriptDebugger::isDebugging, NULL, Reflection::PropertyDescriptor::UI);
|
|
static Reflection::PropDescriptor<ScriptDebugger, bool> prop_IsPaused("IsPaused", category_Data, &ScriptDebugger::isPaused, NULL, Reflection::PropertyDescriptor::UI);
|
|
static Reflection::PropDescriptor<ScriptDebugger, int> prop_CurrentLine("CurrentLine", category_Data, &ScriptDebugger::getCurrentLine, NULL, Reflection::PropertyDescriptor::UI);
|
|
static Reflection::EventDesc<ScriptDebugger, void(int)> event_EncounteredBreak(&ScriptDebugger::encounteredBreak, "EncounteredBreak", "line");
|
|
static Reflection::EventDesc<ScriptDebugger, void()> event_Resuming(&ScriptDebugger::resuming, "Resuming");
|
|
static Reflection::EventDesc<ScriptDebugger, void(shared_ptr<Instance>)> event_BreakpointAdded(&ScriptDebugger::breakpointAdded, "BreakpointAdded", "breakpoint");
|
|
static Reflection::EventDesc<ScriptDebugger, void(shared_ptr<Instance>)> event_BreakpointRemoved(&ScriptDebugger::breakpointRemoved, "BreakpointRemoved", "breakpoint");
|
|
static Reflection::EventDesc<ScriptDebugger, void(shared_ptr<Instance>)> event_WatchAdded(&ScriptDebugger::watchAdded, "WatchAdded", "watch");
|
|
static Reflection::EventDesc<ScriptDebugger, void(shared_ptr<Instance>)> event_WatchRemoved(&ScriptDebugger::watchRemoved, "WatchRemoved", "watch");
|
|
|
|
// The Line property is read-only by design. If we want to make it writable, then we need to watch for changes and update ScriptDebugger::breakpoints
|
|
static Reflection::PropDescriptor<DebuggerBreakpoint, int> prop_Line("Line", category_Data, &DebuggerBreakpoint::getLine, NULL, Reflection::PropertyDescriptor::UI);
|
|
Reflection::BoundProp<int> DebuggerBreakpoint::prop_Line_Data("line", category_Data, &DebuggerBreakpoint::line, Reflection::PropertyDescriptor::STREAMING);
|
|
Reflection::BoundProp<bool> DebuggerBreakpoint::prop_Enabled("IsEnabled", category_Data, &DebuggerBreakpoint::enabled);
|
|
Reflection::BoundProp<std::string> DebuggerBreakpoint::prop_Condition("Condition", category_Data, &DebuggerBreakpoint::condition);
|
|
|
|
Reflection::BoundProp<std::string> DebuggerWatch::prop_Expression("Expression", category_Data, &DebuggerWatch::expression);
|
|
static Reflection::BoundFuncDesc<DebuggerWatch, void()> func_CheckExpressionSyntax(&DebuggerWatch::checkExpressionSyntax, "CheckSyntax", Security::None);
|
|
REFLECTION_END();
|
|
|
|
static DebuggerManager* gDebuggerManager = NULL;
|
|
static void doBasicSingleton()
|
|
{
|
|
static shared_ptr<DebuggerManager> sing = Creatable<Instance>::create<DebuggerManager>();
|
|
gDebuggerManager = sing.get();
|
|
}
|
|
|
|
static void initDebuggerManagerSingleton()
|
|
{
|
|
doBasicSingleton();
|
|
}
|
|
|
|
DebuggerManager& DebuggerManager::singleton()
|
|
{
|
|
static boost::once_flag flag = BOOST_ONCE_INIT;
|
|
boost::call_once(&initDebuggerManagerSingleton, flag);
|
|
return *gDebuggerManager;
|
|
}
|
|
|
|
DebuggerManager::DebuggerManager(void)
|
|
:enabled(false)
|
|
,dataModel(NULL)
|
|
,executionMode(ExecutionMode_Continue)
|
|
,breakOnErrorMode(BreakOnErrorMode_Never)
|
|
,resuming(false)
|
|
,scriptAutoResume(true)
|
|
{
|
|
setName(sDebuggerManager);
|
|
}
|
|
|
|
DebuggerManager::~DebuggerManager(void)
|
|
{
|
|
}
|
|
|
|
void DebuggerManager::setDataModel(RBX::DataModel *pDataModel)
|
|
{
|
|
if (pDataModel == dataModel)
|
|
return;
|
|
|
|
if (dataModel)
|
|
removeAllChildren();
|
|
|
|
errorSignalConnection.disconnect();
|
|
descendantAddedSignalConnection.disconnect();
|
|
breakOnErrorMode = BreakOnErrorMode_Never;
|
|
debuggersLookup.clear();
|
|
debuggers.clear();
|
|
unaddedDebuggers.clear();
|
|
reset();
|
|
|
|
dataModel = pDataModel;
|
|
|
|
if (dataModel)
|
|
descendantAddedSignalConnection = dataModel->getOrCreateDescendantAddedSignal()->connect(boost::bind(&DebuggerManager::addUnaddedDebuggerForAddedDescendant, this, _1));
|
|
}
|
|
|
|
RBX::DataModel* DebuggerManager::getDataModel()
|
|
{ return dataModel; }
|
|
|
|
void DebuggerManager::reset()
|
|
{
|
|
specialBreakpoint.reset();
|
|
pausedThreads.clear();
|
|
executionMode = ExecutionMode_Continue;
|
|
resuming = false;
|
|
scriptAutoResume = true;
|
|
}
|
|
|
|
void DebuggerManager::enableDebugging()
|
|
{
|
|
enabled = true;
|
|
raiseChanged(prop_Enabled);
|
|
}
|
|
|
|
void DebuggerManager::setBreakOnErrorMode(BreakOnErrorMode mode)
|
|
{
|
|
if (!FFlag::LuaDebuggerBreakOnError || (mode == breakOnErrorMode) || !dataModel)
|
|
return;
|
|
|
|
errorSignalConnection.disconnect();
|
|
|
|
breakOnErrorMode = mode;
|
|
|
|
// connect with ScriptContext signal, using this we will catch errors which are reported in Output window
|
|
if (breakOnErrorMode != BreakOnErrorMode_Never)
|
|
{
|
|
ScriptContext* pScriptContext = ServiceProvider::find<ScriptContext>(dataModel);
|
|
if (pScriptContext)
|
|
errorSignalConnection = pScriptContext->scriptErrorDetected.connect(boost::bind(&DebuggerManager::onErrorSignal, this, _1));
|
|
}
|
|
|
|
// update hooks: this will make sure correct mask is set for lua threads
|
|
for (Debuggers::iterator iter = debuggers.begin(); iter != debuggers.end(); ++iter)
|
|
iter->second->updateHook();
|
|
}
|
|
|
|
shared_ptr<const Instances> DebuggerManager::getDebuggers_Reflection()
|
|
{
|
|
shared_ptr<Instances> result = rbx::make_shared<Instances>(debuggers.size());
|
|
int i = 0;
|
|
for (Debuggers::const_iterator iter = debuggers.begin(); iter != debuggers.end(); ++iter, ++i)
|
|
(*result)[i] = shared_from(iter->second);
|
|
return result;
|
|
}
|
|
|
|
ScriptDebugger* DebuggerManager::findDebugger( lua_State* L )
|
|
{
|
|
DebuggersLookup::const_iterator iter = debuggersLookup.find(L);
|
|
if (iter != debuggersLookup.end())
|
|
return iter->second;
|
|
|
|
ScriptDebugger* scriptDebugger = findDebugger(RobloxExtraSpace::get(L)->script.lock().get());
|
|
if (scriptDebugger)
|
|
debuggersLookup[L] = scriptDebugger;
|
|
|
|
return scriptDebugger;
|
|
}
|
|
|
|
bool DebuggerManager::askForbidChild(const Instance* newChild) const
|
|
{
|
|
return !Instance::fastDynamicCast<ScriptDebugger>(newChild);
|
|
}
|
|
|
|
void DebuggerManager::verifyAddChild(const Instance* newChild) const
|
|
{
|
|
if (!askForbidChild(newChild))
|
|
return;
|
|
|
|
throw std::runtime_error("Only ScriptDebuggers can be children of DebuggerManager");
|
|
}
|
|
|
|
|
|
shared_ptr<Instance> DebuggerManager::addDebugger_Reflection( shared_ptr<Instance> script )
|
|
{
|
|
if (Script* s = Instance::fastDynamicCast<Script>(script.get()))
|
|
return addDebugger(s);
|
|
throw std::runtime_error("Can only add debugger for a Script");
|
|
}
|
|
|
|
|
|
ScriptDebugger* DebuggerManager::findDebugger( Instance* script )
|
|
{
|
|
if (!debuggers.empty() && script)
|
|
{
|
|
Debuggers::const_iterator iter = debuggers.find(script);
|
|
if (iter != debuggers.end())
|
|
return iter->second;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
shared_ptr<ScriptDebugger> DebuggerManager::addDebugger( Instance* script )
|
|
{
|
|
checkDebugging();
|
|
|
|
if (ScriptDebugger* debugger = findDebugger(script))
|
|
return shared_from(debugger);
|
|
|
|
shared_ptr<ScriptDebugger> debugger = Creatable<Instance>::create<ScriptDebugger>(boost::ref(*script));
|
|
debugger->setParent(this);
|
|
return debugger;
|
|
}
|
|
|
|
void DebuggerManager::addDebugger(shared_ptr<ScriptDebugger> debugger)
|
|
{
|
|
if (!debugger || !debugger->getScript())
|
|
return;
|
|
|
|
// check if debugger already added
|
|
RBX::Instance* script = debugger->getScript();
|
|
if (debuggers.find(script) != debuggers.end())
|
|
return;
|
|
|
|
// if script has been added into datamodel, directly add debugger
|
|
if (dataModel->isAncestorOf(script))
|
|
debugger->setParent(this);
|
|
// else wait till it get's added
|
|
else
|
|
unaddedDebuggers[script] = debugger;
|
|
}
|
|
|
|
void DebuggerManager::addUnaddedDebuggerForAddedDescendant(shared_ptr<RBX::Instance> instance)
|
|
{
|
|
if (instance && (instance->isA<RBX::Script>() || instance->isA<RBX::ModuleScript>()))
|
|
{
|
|
// check if we've an unadded debugger for the added instance
|
|
UnaddedDebuggers::iterator iter = unaddedDebuggers.find(instance.get());
|
|
if (iter != unaddedDebuggers.end())
|
|
{
|
|
RBXASSERT(debuggers.find(instance.get()) == debuggers.end());
|
|
(iter->second)->setParent(this);
|
|
unaddedDebuggers.erase(iter);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DebuggerManager::populateForLookup(lua_State* L, ScriptDebugger* debugger)
|
|
{
|
|
debuggersLookup[L] = debugger;
|
|
}
|
|
|
|
void DebuggerManager::onChildRemoved( Instance* child )
|
|
{
|
|
if (ScriptDebugger* debugger = Instance::fastDynamicCast<ScriptDebugger>(child))
|
|
{
|
|
debuggers.erase(debugger->getScript());
|
|
|
|
for (std::list<Lua::WeakThreadRef>::iterator iter = pausedThreads.begin(); iter != pausedThreads.end();)
|
|
{
|
|
if (debugger->isPausedThread(reinterpret_cast<uintptr_t>( iter->lock().get() )))
|
|
{
|
|
pausedThreads.erase(iter++);
|
|
}
|
|
else
|
|
{
|
|
++iter;
|
|
}
|
|
}
|
|
|
|
for (DebuggersLookup::iterator iter = debuggersLookup.begin(); iter != debuggersLookup.end();)
|
|
{
|
|
if (iter->second == debugger)
|
|
{
|
|
debuggersLookup.erase(iter++);
|
|
}
|
|
else
|
|
{
|
|
++iter;
|
|
}
|
|
}
|
|
|
|
debuggerRemoved(shared_from(debugger));
|
|
}
|
|
|
|
Super::onChildRemoved(child);
|
|
}
|
|
|
|
void DebuggerManager::addScriptDebugger(Instance* instance)
|
|
{
|
|
if (ScriptDebugger* debugger = Instance::fastDynamicCast<ScriptDebugger>(instance))
|
|
{
|
|
if (debugger->getScript() != 0)
|
|
{
|
|
RBXASSERT(debuggers.find(debugger->getScript()) == debuggers.end());
|
|
debuggers[debugger->getScript()] = debugger;
|
|
debuggerAdded(shared_from(debugger));
|
|
}
|
|
}
|
|
}
|
|
|
|
void DebuggerManager::onChildAdded( Instance* child )
|
|
{
|
|
Super::onChildAdded(child);
|
|
addScriptDebugger(child);
|
|
}
|
|
|
|
void DebuggerManager::onChildChanged(Instance* instance, const PropertyChanged& event)
|
|
{
|
|
Super::onChildChanged(instance, event);
|
|
|
|
if (event.getDescriptor() == prop_ScriptPath)
|
|
addScriptDebugger(instance);
|
|
}
|
|
|
|
void DebuggerManager::onErrorSignal(lua_State* L)
|
|
{
|
|
ScriptDebugger* pDebugger = findDebugger(L);
|
|
if (pDebugger && !pDebugger->hasError())
|
|
{
|
|
pDebugger->handleError(L);
|
|
errorThreads.push_back(L);
|
|
}
|
|
}
|
|
|
|
void DebuggerManager::pause()
|
|
{
|
|
executionMode = ExecutionMode_Break;
|
|
}
|
|
|
|
void DebuggerManager::resume()
|
|
{
|
|
executionMode = ExecutionMode_Continue;
|
|
|
|
// update error threads, resumeThread will just reset the debugger data
|
|
while (!errorThreads.empty())
|
|
{
|
|
Lua::WeakThreadRef weakref = *(errorThreads.begin());
|
|
if (RBX::Lua::ThreadRef thread = weakref.lock())
|
|
{
|
|
ScriptDebugger* debugger = findDebugger(thread);
|
|
if (debugger && debugger->hasError())
|
|
debugger->resumeThread(thread.get());
|
|
}
|
|
errorThreads.remove(weakref);
|
|
}
|
|
|
|
pausedThreads.insert(pausedThreads.begin(), resumingPausedThreads.begin(), resumingPausedThreads.end());
|
|
resumingPausedThreads.clear();
|
|
|
|
resuming = true;
|
|
bool evalLineHookForCurrentLine = false;
|
|
try
|
|
{
|
|
while (!pausedThreads.empty())
|
|
{
|
|
Lua::WeakThreadRef weakref = *(pausedThreads.begin());
|
|
if (RBX::Lua::ThreadRef thread = weakref.lock())
|
|
{
|
|
ScriptDebugger* debugger = findDebugger(thread);
|
|
if (debugger)
|
|
{
|
|
ScriptContext::Result result = debugger->resumeThread(thread.get(), evalLineHookForCurrentLine);
|
|
// if there's an error during resume, remove the thread from list so we do not resume it again
|
|
if (result == ScriptContext::Error)
|
|
{
|
|
pausedThreads.remove(weakref);
|
|
break;
|
|
}
|
|
// if thread has yielded
|
|
else if (result == ScriptContext::Yield)
|
|
{
|
|
// do nothing if we have break mode enabled, get out of loop
|
|
if (executionMode == ExecutionMode_Break)
|
|
{
|
|
break;
|
|
}
|
|
|
|
// if we have a special breakpoint without a baseThread, then set the current yielded thread
|
|
// this will make sure special breakpoint will handle instructions specific to this thread only
|
|
if (specialBreakpoint && !specialBreakpoint->baseThread)
|
|
{
|
|
specialBreakpoint->baseThread = thread;
|
|
if (!dynamic_cast<StepOutBreakpoint*>(specialBreakpoint.get()))
|
|
break;
|
|
}
|
|
|
|
// TODO: we might want to break resume here
|
|
// if root thread is yielded (using wait) and there's no special breakpoint, then how do we know it's resumed via yieldedthreads queue?
|
|
// knowing this we can resume other paused threads also, this will make sure we do not have incorrect stepping in child threads.
|
|
}
|
|
// thread is successfully executed, check if we need to reset specialbreakpoint
|
|
if ((specialBreakpoint && specialBreakpoint->baseThread == thread.get()) || debugger->isRootThreadResumed())
|
|
specialBreakpoint.reset();
|
|
// now we need to force evaluation of line hook for current line in each debugger (just in case if we miss breakpoint set on current line)
|
|
evalLineHookForCurrentLine = true;
|
|
}
|
|
}
|
|
pausedThreads.remove(weakref);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
resuming = false;
|
|
}
|
|
|
|
void DebuggerManager::stepOver()
|
|
{
|
|
if (pausedThreads.empty() && resumingPausedThreads.empty())
|
|
throw std::runtime_error("Can't step while running");
|
|
|
|
specialBreakpoint.reset(new StepOverBreakpoint());
|
|
if (scriptAutoResume)
|
|
resume();
|
|
}
|
|
|
|
void DebuggerManager::stepInto()
|
|
{
|
|
if (pausedThreads.empty() && resumingPausedThreads.empty())
|
|
throw std::runtime_error("Can't step while running");
|
|
|
|
specialBreakpoint.reset(new StepInBreakpoint());
|
|
if (scriptAutoResume)
|
|
resume();
|
|
}
|
|
|
|
void DebuggerManager::stepOut()
|
|
{
|
|
if (pausedThreads.empty() && resumingPausedThreads.empty())
|
|
throw std::runtime_error("Can't step while running");
|
|
|
|
specialBreakpoint.reset(new StepOutBreakpoint());
|
|
if (scriptAutoResume)
|
|
resume();
|
|
}
|
|
|
|
void DebuggerManager::hook( lua_State* L, lua_Debug *ar )
|
|
{
|
|
if (ar->event == LUA_HOOKCOUNT)
|
|
{
|
|
ScriptContext::hook(L, ar);
|
|
}
|
|
else
|
|
{
|
|
gDebuggerManager->onHook(L, ar);
|
|
}
|
|
}
|
|
|
|
void DebuggerManager::onHook(lua_State* L, lua_Debug *ar)
|
|
{
|
|
ScriptDebugger* debugger = findDebugger(L);
|
|
if (!debugger)
|
|
return;
|
|
|
|
if (debugger->handleHook(L, ar) || debugger->isPausedThread(reinterpret_cast<uintptr_t>(L)))
|
|
return;
|
|
|
|
bool paused = false;
|
|
// first verify if we are running in breakmode
|
|
if (executionMode == ExecutionMode_Break)
|
|
{
|
|
if (ar->event == LUA_HOOKLINE)
|
|
{
|
|
debugger->debuggerBreak(L, ar);
|
|
paused = true;
|
|
}
|
|
}
|
|
// second is to check for a special breakpoint
|
|
else if (specialBreakpoint)
|
|
{
|
|
lua_getinfo(L, "Sf", ar);
|
|
|
|
if (specialBreakpoint->hitTest(L, ar))
|
|
{
|
|
debugger->debuggerBreak(L, ar);
|
|
executionMode = ExecutionMode_Break;
|
|
paused = true;
|
|
}
|
|
}
|
|
|
|
// last check for a user created breakpoint
|
|
if (!paused && (ar->event == LUA_HOOKLINE) && debugger->onLineHook(L, ar))
|
|
{
|
|
executionMode = ExecutionMode_Break;
|
|
paused = true;
|
|
}
|
|
|
|
if (paused)
|
|
{
|
|
// reset special breakpoint as we do not need it anymore
|
|
specialBreakpoint.reset();
|
|
// make sure we do not add an already paused thread in the list
|
|
if (std::find(pausedThreads.begin(), pausedThreads.end(), L) != pausedThreads.end())
|
|
return;
|
|
// if a threads gets paused during resume then it must be pushed on the top of the list else push it in back
|
|
resuming ? resumingPausedThreads.push_back(L) : pausedThreads.push_back(L);
|
|
//RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "paused debugger = %d", L);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// ScriptDebugger
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
ScriptDebugger::ScriptDebugger( Instance& script )
|
|
:currentLine(0)
|
|
,breakpointHookData(NULL)
|
|
,globalRawScriptPtr(NULL)
|
|
,prevFuncTable(NULL)
|
|
,prevRawScriptPtr(NULL)
|
|
,ignoreDebuggerBreak(false)
|
|
,currentThreadID(0)
|
|
,rootThreadResumed(false)
|
|
{
|
|
setName(RBX::format("%sDebugger", script.getName().c_str()));
|
|
setScript(&script);
|
|
}
|
|
|
|
ScriptDebugger::~ScriptDebugger()
|
|
{
|
|
scriptStartedConnection.disconnect();
|
|
scriptStoppedConnection.disconnect();
|
|
scriptParentChangedConnection.disconnect();
|
|
scriptClonedConnection.disconnect();
|
|
}
|
|
|
|
shared_ptr<const Instances> ScriptDebugger::getBreakpoints_Reflection()
|
|
{
|
|
shared_ptr<Instances> result = rbx::make_shared<Instances>(breakpoints.size());
|
|
int i = 0;
|
|
for (Breakpoints::const_iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter, ++i)
|
|
(*result)[i] = shared_from(iter->second);
|
|
return result;
|
|
}
|
|
|
|
|
|
shared_ptr<DebuggerWatch> ScriptDebugger::addWatch( std::string expression )
|
|
{
|
|
shared_ptr<DebuggerWatch> watch = Creatable<Instance>::create<DebuggerWatch>(expression);
|
|
watch->setParent(this);
|
|
return watch;
|
|
}
|
|
|
|
|
|
shared_ptr<Instance> ScriptDebugger::addWatch_Reflection( std::string expression )
|
|
{
|
|
return addWatch(expression);
|
|
}
|
|
|
|
|
|
shared_ptr<const Instances> ScriptDebugger::getWatches_Reflection()
|
|
{
|
|
shared_ptr<Instances> result = rbx::make_shared<Instances>(watches.size());
|
|
int i = 0;
|
|
for (Watches::const_iterator iter = watches.begin(); iter != watches.end(); ++iter, ++i)
|
|
(*result)[i] = shared_from(*iter);
|
|
return result;
|
|
}
|
|
|
|
Reflection::Variant ScriptDebugger::getWatchValue_Reflection( shared_ptr<Instance> instance )
|
|
{
|
|
if (DebuggerWatch* watch = Instance::fastDynamicCast<DebuggerWatch>(instance.get()))
|
|
return getWatchValue(watch);
|
|
throw std::runtime_error("bad watch argument");
|
|
}
|
|
|
|
static int setIndexInfo(lua_State * L)
|
|
{
|
|
RBX::StandardOut::singleton()->print(RBX::MESSAGE_INFO, "setIndexInfo");
|
|
return 0;
|
|
}
|
|
|
|
static int getIndexInfo(lua_State * L)
|
|
{
|
|
//check if there's a key value set?
|
|
const char * key = lua_tostring(L, 2);
|
|
if (!key)
|
|
return 0;
|
|
|
|
// Find the extra 'arguments' in the metatable
|
|
|
|
lua_getmetatable(L, 1);
|
|
int metatableIndex = lua_gettop(L);
|
|
|
|
// Get the original environment table
|
|
lua_pushstring(L, "env");
|
|
lua_rawget(L, metatableIndex);
|
|
int envIndex = lua_gettop(L);
|
|
|
|
// Get the chunk function that was originally called
|
|
lua_pushstring(L, "function");
|
|
lua_rawget(L, metatableIndex);
|
|
//int functionIndex = lua_gettop(L);
|
|
|
|
// Get the current stack frame
|
|
lua_pushstring(L, "frame");
|
|
lua_rawget(L, metatableIndex);
|
|
int frame = (int) lua_tointeger(L, -1);
|
|
|
|
// Get the thread
|
|
lua_pushstring(L, "thread");
|
|
lua_rawget(L, metatableIndex);
|
|
lua_State * parentThread = lua_tothread(L, -1);
|
|
|
|
//
|
|
// Check in locals first
|
|
//
|
|
|
|
// If we're examining the main thread we need to skip over the watch expression call, and the metatable __index call
|
|
if(parentThread == L)
|
|
frame += 2;
|
|
|
|
lua_Debug debugInfo;
|
|
memset(&debugInfo, 0, sizeof(lua_Debug));
|
|
if(lua_getstack(parentThread, frame, &debugInfo) == 1 && lua_getinfo(parentThread, "uf", &debugInfo))
|
|
{
|
|
int funcIndex = lua_gettop(parentThread);
|
|
|
|
// Look in the upvalues
|
|
if(debugInfo.nups > 0)
|
|
{
|
|
for(int index = 1; index <= debugInfo.nups; ++index)
|
|
{
|
|
const char * name = lua_getupvalue(parentThread, funcIndex, index);
|
|
if(name != NULL && strcmp(name, key) == 0)
|
|
{
|
|
// Move the value across
|
|
lua_xmove(parentThread, L, 1);
|
|
|
|
// Pop the function off the stack
|
|
lua_remove(parentThread, funcIndex);
|
|
|
|
// Don't bother removing the other stuff on the stack, lua will do it for us
|
|
return 1;
|
|
}
|
|
lua_pop(parentThread, 1);
|
|
}
|
|
}
|
|
|
|
// Check in the locals
|
|
int index = 1;
|
|
const char * name;
|
|
while( (name = lua_getlocal(parentThread, &debugInfo, index++)) != NULL)
|
|
{
|
|
if(strcmp(name, key) == 0)
|
|
{
|
|
// Move the value across
|
|
lua_xmove(parentThread, L, 1);
|
|
|
|
// Pop the function off the stack
|
|
lua_remove(parentThread, funcIndex);
|
|
|
|
// Don't bother removing the other stuff on the stack, lua will do it for us
|
|
return 1;
|
|
}
|
|
lua_pop(parentThread, 1);
|
|
}
|
|
|
|
// This might trigger an error
|
|
lua_pushvalue(L, 2);
|
|
lua_gettable(L, envIndex);
|
|
|
|
// If it's nil, leave it as the return value anyway
|
|
return 1;
|
|
}
|
|
else
|
|
{
|
|
// There's no lua function running, so just look in the globals
|
|
lua_getglobal(L, key);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static void setEvalThreadEnvironment(lua_State * parentThread, lua_State* evalThread, int stackDepth)
|
|
{
|
|
int functionIndex = lua_gettop(evalThread);
|
|
lua_checkstack(evalThread, 6);
|
|
|
|
// Create environment table
|
|
lua_createtable(evalThread, 0, 2);
|
|
int envIndex = lua_gettop(evalThread);
|
|
|
|
// Create metatable
|
|
lua_createtable(evalThread, 0, 5);
|
|
int metatableIndex = lua_gettop(evalThread);
|
|
|
|
lua_pushstring(evalThread, "__index");
|
|
lua_pushcfunction(evalThread, &getIndexInfo);
|
|
lua_settable(evalThread, metatableIndex);
|
|
|
|
lua_pushstring(evalThread, "__newindex");
|
|
lua_pushcfunction(evalThread, &setIndexInfo);
|
|
lua_settable(evalThread, metatableIndex);
|
|
|
|
// Store the stack index
|
|
lua_pushstring(evalThread, "frame");
|
|
lua_pushnumber(evalThread, stackDepth);
|
|
lua_settable(evalThread, metatableIndex);
|
|
|
|
// Store the original thread (this will be required to access stack information)
|
|
lua_pushstring(evalThread, "thread");
|
|
lua_pushthread(parentThread);
|
|
lua_xmove(parentThread, evalThread, 1);
|
|
lua_settable(evalThread, metatableIndex);
|
|
|
|
// Store the function
|
|
lua_pushstring(evalThread, "function");
|
|
lua_pushvalue(evalThread, functionIndex);
|
|
lua_settable(evalThread, metatableIndex);
|
|
|
|
// Store the original function environment
|
|
lua_pushstring(evalThread, "env");
|
|
lua_Debug debugInfo;
|
|
memset(&debugInfo, 0, sizeof(lua_Debug));
|
|
if(lua_getstack(parentThread, stackDepth, &debugInfo) == 1 && lua_getinfo(parentThread, "f", &debugInfo))
|
|
{
|
|
int funcIndex = lua_gettop(parentThread);
|
|
lua_getfenv(parentThread, funcIndex);
|
|
lua_xmove(parentThread, evalThread, 1);
|
|
lua_remove(parentThread, funcIndex);
|
|
}
|
|
else
|
|
{
|
|
// If there's nothing running, fall back on the globals table.
|
|
lua_pushvalue(evalThread, LUA_GLOBALSINDEX);
|
|
}
|
|
|
|
lua_settable(evalThread, metatableIndex);
|
|
|
|
// Assign the metatable to the environment
|
|
lua_setmetatable(evalThread, envIndex);
|
|
|
|
// Set our environment for the function
|
|
lua_setfenv(evalThread, functionIndex);
|
|
|
|
//again set the function index at the top of the stack
|
|
lua_settop(evalThread, functionIndex);
|
|
}
|
|
|
|
Reflection::Variant DebuggerManager::readWatchValue(std::string expression, int stackDepth, lua_State* L)
|
|
{
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
|
|
lua_State* evalThread = lua_newthread(L);
|
|
lua_sethook(evalThread, NULL, 0, 0); // Prevent re-entrancy
|
|
|
|
Lua::ScopedPopper pop(L, 1);
|
|
|
|
RBXASSERT_BALLANCED_LUA_STACK2(evalThread);
|
|
|
|
const std::string code = "return " + expression;
|
|
bool loaded;
|
|
loaded = LuaVM::load(evalThread, ProtectedString::fromTrustedSource(code), "") == 0;
|
|
Lua::ScopedPopper pop2(evalThread, 1);
|
|
|
|
if (!loaded)
|
|
{
|
|
std::string err = Lua::safe_lua_tostring(evalThread, -1);
|
|
throw RBX::runtime_error("syntax error: %s", err.c_str());
|
|
}
|
|
|
|
// set new function environment for evaluation thread so we can access locals/upvalues/environment of the original context
|
|
setEvalThreadEnvironment(L, evalThread, stackDepth);
|
|
|
|
if (lua_pcall(evalThread, 0, 1, 0))
|
|
{
|
|
//const char* message = lua_tostring(evalThread, -1);
|
|
//throw RBX::runtime_error("runtime error: %s", message);
|
|
return std::string("*** Value not found ***");
|
|
}
|
|
|
|
Reflection::Variant result;
|
|
try
|
|
{
|
|
Lua::LuaArguments::get(evalThread, -1, result, false);
|
|
}
|
|
catch (std::runtime_error const& exp)
|
|
{
|
|
return std::string(RBX::format("*** %s ***", exp.what()));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Reflection::Variant ScriptDebugger::getWatchValue( DebuggerWatch* watch, int stackFrame )
|
|
{
|
|
Reflection::Variant result;
|
|
|
|
if (hasError())
|
|
{
|
|
// for error thread, we cannot do a resume. since it is not yielded try querying values directly.
|
|
if (Lua::ThreadRef threadRef = errorThread.lock())
|
|
{
|
|
result = DebuggerManager::readWatchValue(watch->getExpression(), stackFrame, threadRef);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = withPausedThread<Reflection::Variant>(boost::bind(&DebuggerManager::readWatchValue, watch->getExpression(), stackFrame, _1));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Reflection::Variant ScriptDebugger::getKeyValue(std::string key, int stackFrame)
|
|
{
|
|
Reflection::Variant result;
|
|
|
|
if (hasError())
|
|
{
|
|
// for error thread, we cannot do a resume. since it is not yielded try querying values directly.
|
|
if (Lua::ThreadRef threadRef = errorThread.lock())
|
|
{
|
|
result = DebuggerManager::readWatchValue(key, stackFrame, threadRef);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = withPausedThread<Reflection::Variant>(boost::bind(&DebuggerManager::readWatchValue, key, stackFrame, _1));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
template<class R>
|
|
void ScriptDebugger::withPausedThreadHook( lua_State* L, lua_Debug *ar, boost::function<R(lua_State* L, lua_Debug *ar)> f, R& r, shared_ptr<std::string>& error)
|
|
{
|
|
RBXASSERT(ar->event == LUA_HOOKLINE);
|
|
RBXASSERT(currentLine == ar->currentline);
|
|
|
|
try
|
|
{
|
|
r = f(L, ar);
|
|
}
|
|
catch (RBX::base_exception& ex)
|
|
{
|
|
error = rbx::make_shared<std::string>(ex.what());
|
|
}
|
|
|
|
RBXASSERT(pausedThread.lock() == L);
|
|
//pausedThread = L;
|
|
RBXASSERT(!RobloxExtraSpace::get(L)->yieldCaptured);
|
|
RobloxExtraSpace::get(L)->yieldCaptured = true;
|
|
lua_yield(L, 0);
|
|
}
|
|
|
|
|
|
|
|
template<class R>
|
|
R ScriptDebugger::withPausedThread( boost::function<R(lua_State* L, lua_Debug *ar)> f )
|
|
{
|
|
// When pausedThread is set, then Lua stack is not usable.
|
|
// Therefore, we resume the thread back into the hook and run our code there.
|
|
// See http://lua-users.org/lists/lua-l/2012-06/msg00169.html
|
|
|
|
if (Lua::ThreadRef thread = pausedThread.lock())
|
|
{
|
|
if (breakpointHookData)
|
|
{
|
|
// We haven't yielded yet, so go ahead and make the call directly
|
|
return f(thread, breakpointHookData);
|
|
}
|
|
|
|
R r;
|
|
shared_ptr<std::string> error;
|
|
RBXASSERT(!hookFunction);
|
|
hookFunction = boost::bind(&ScriptDebugger::withPausedThreadHook<R>, this, _1, _2, f, boost::ref(r), boost::ref(error));
|
|
try
|
|
{
|
|
if (Lua::ThreadRef thread = pausedThread.lock())
|
|
{
|
|
RBXASSERT_BALLANCED_LUA_STACK(thread);
|
|
ScriptContext::getContext(thread).resume(thread, 0);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
hookFunction.clear();
|
|
throw;
|
|
}
|
|
hookFunction.clear();
|
|
|
|
if (error)
|
|
throw std::runtime_error(*error);
|
|
|
|
return r;
|
|
}
|
|
else
|
|
throw std::runtime_error("Cannot perform this operation unless the thread is paused");
|
|
}
|
|
|
|
void ScriptDebugger::debuggerBreak( lua_State* L, lua_Debug *ar )
|
|
{
|
|
if (ignoreDebuggerBreak)
|
|
{
|
|
StandardOut::singleton()->print(MESSAGE_WARNING, "Breakpoints are not supported for the executed script code");
|
|
return;
|
|
}
|
|
|
|
lua_getinfo(L, "S", ar);
|
|
|
|
switch (ar->event)
|
|
{
|
|
case LUA_HOOKLINE:
|
|
currentLine = ar->currentline;
|
|
break;
|
|
case LUA_HOOKCALL:
|
|
currentLine = ar->linedefined;
|
|
break;
|
|
case LUA_HOOKRET:
|
|
case LUA_HOOKTAILRET:
|
|
currentLine = ar->lastlinedefined;
|
|
break;
|
|
}
|
|
|
|
RBXASSERT(currentLine > 0);
|
|
|
|
//RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Encountered breakpoint: %s line %d", ar->short_src, currentLine);
|
|
|
|
pausedThread = L;
|
|
currentThreadID = reinterpret_cast<uintptr_t>(L);
|
|
|
|
PausedThreadData data;
|
|
data.pausedLine = currentLine;
|
|
data.thread = L;
|
|
data.callStack = getStack();
|
|
|
|
pausedThreads[reinterpret_cast<uintptr_t>(L)] = data;
|
|
|
|
breakpointHookData = ar;
|
|
encounteredBreak(currentLine);
|
|
breakpointHookData = NULL;
|
|
|
|
if (pausedThread.empty())
|
|
{
|
|
// If pausedThread was reset during encounteredBreak, then don't yield. We're done.
|
|
//RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Resuming: %s line %d", ar->short_src, ar->currentline);
|
|
resuming();
|
|
currentLine = 0;
|
|
}
|
|
else
|
|
{
|
|
RBXASSERT(!RobloxExtraSpace::get(L)->yieldCaptured);
|
|
RobloxExtraSpace::get(L)->yieldCaptured = true;
|
|
lua_yield(L, 0);
|
|
}
|
|
}
|
|
|
|
bool ScriptDebugger::handleHook(lua_State* L, lua_Debug *ar)
|
|
{
|
|
if (hookFunction)
|
|
{
|
|
hookFunction(L, ar);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
shared_ptr<Reflection::ValueMap> ScriptDebugger::readLocals(int stackIndex, lua_State* L)
|
|
{
|
|
lua_Debug ar;
|
|
if (lua_getstack(L, stackIndex, &ar) == 0)
|
|
throw std::runtime_error("stackIndex out of range");
|
|
|
|
shared_ptr<Reflection::ValueMap> locals = rbx::make_shared<Reflection::ValueMap>();
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
int n = 1;
|
|
while (const char* name = lua_getlocal(L, &ar, n++))
|
|
{
|
|
if (name[0] > 0)
|
|
if (strcmp(name, "(*temporary)") != 0)
|
|
{
|
|
Reflection::Variant value;
|
|
Lua::LuaArguments::get(L, -1, value, false);
|
|
(*locals)[name] = value;
|
|
}
|
|
lua_pop(L, 1); // pop value
|
|
}
|
|
return locals;
|
|
}
|
|
|
|
shared_ptr<Reflection::ValueMap> ScriptDebugger::readGlobals(lua_State* L)
|
|
{
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
|
|
#if 0
|
|
lua_Debug ar;
|
|
if (!lua_getstack(L, stackIndex, &ar))
|
|
throw std::runtime_error("stackIndex out of range");
|
|
if (!lua_getinfo(L, "f", &ar))
|
|
throw std::runtime_error("failed to get function info");
|
|
// The stack now contains the function we are interested in
|
|
|
|
lua_getfenv(L, -1);
|
|
// The stack now contains the the environment table
|
|
const int t = lua_gettop(L);
|
|
#else
|
|
const int t = LUA_GLOBALSINDEX;
|
|
lua_getfenv(L, t);
|
|
#endif
|
|
shared_ptr<Reflection::ValueMap> result = rbx::make_shared<Reflection::ValueMap>();
|
|
|
|
lua_pushnil(L); /* first key */
|
|
while (lua_next(L, t))
|
|
{
|
|
if (lua_isstring(L, -2))
|
|
{
|
|
const char* name = lua_tostring(L, -2);
|
|
RBXASSERT(result->find(name) == result->end());
|
|
|
|
Reflection::Variant value;
|
|
Lua::LuaArguments::get(L, -1, value, false);
|
|
(*result)[name] = value;
|
|
}
|
|
|
|
/* removes 'value'; keeps 'key' for next iteration */
|
|
lua_pop(L, 1);
|
|
}
|
|
|
|
lua_pop(L, 1);
|
|
|
|
return result;
|
|
}
|
|
|
|
shared_ptr<Reflection::ValueMap> ScriptDebugger::readUpvalues(int stackIndex, lua_State* L)
|
|
{
|
|
lua_Debug ar;
|
|
if (lua_getstack(L, stackIndex, &ar) == 0)
|
|
throw std::runtime_error("stackIndex out of range");
|
|
|
|
shared_ptr<Reflection::ValueMap> arguments = rbx::make_shared<Reflection::ValueMap>();
|
|
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
int result = lua_getinfo(L, "fu", &ar);
|
|
RBXASSERT(result);
|
|
// stack contains current function
|
|
|
|
int n = 1;
|
|
while (const char* name = lua_getupvalue(L, -1, n++))
|
|
{
|
|
if (name[0] > 0)
|
|
{
|
|
Reflection::Variant value;
|
|
Lua::LuaArguments::get(L, -1, value, false);
|
|
RBXASSERT(arguments->find(name) == arguments->end());
|
|
(*arguments)[name] = value;
|
|
}
|
|
lua_pop(L, 1); // pop value
|
|
}
|
|
|
|
lua_pop(L, 1); // pop current function
|
|
|
|
return arguments;
|
|
}
|
|
|
|
static const char *traverseGlobals(lua_State *L, const TValue *o)
|
|
{
|
|
Table *globalTable = hvalue(gt(L));
|
|
if (globalTable)
|
|
{
|
|
int tableSize = sizenode(globalTable);
|
|
while (tableSize--)
|
|
{
|
|
Node *node = gnode(globalTable, tableSize);
|
|
if (luaO_rawequalObj(o, gval(node)) && ttisstring(gkey(node)))
|
|
return getstr(tsvalue(gkey(node)));
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void populateFunctionName(lua_State *L, ScriptDebugger::FunctionInfo& funcInfo)
|
|
{
|
|
StkId func = L->top - 1;
|
|
if (func && isLfunction(func))
|
|
{
|
|
const char* globalName = traverseGlobals(L, func);
|
|
if (globalName)
|
|
{
|
|
funcInfo.name = globalName;
|
|
funcInfo.namewhat = "global";
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<ScriptDebugger::FunctionInfo> ScriptDebugger::readStack(lua_State* L)
|
|
{
|
|
std::vector<FunctionInfo> result;
|
|
|
|
for (int frame=0; ; ++frame)
|
|
{
|
|
lua_Debug ar;
|
|
if (lua_getstack(L, frame, &ar) == 0)
|
|
break;
|
|
int r = lua_getinfo(L, "nSlf", &ar);
|
|
RBXASSERT(r);
|
|
FunctionInfo fi;
|
|
fi.frame = frame;
|
|
fi.currentline = ar.currentline;
|
|
fi.lastlinedefined = ar.lastlinedefined;
|
|
fi.linedefined = ar.linedefined;
|
|
if (ar.name)
|
|
fi.name = ar.name;
|
|
if (ar.what)
|
|
fi.what = ar.what;
|
|
if (ar.namewhat)
|
|
fi.namewhat = ar.namewhat;
|
|
if (ar.short_src)
|
|
fi.short_src = ar.short_src;
|
|
fi.script = shared_from(getScriptForLuaState(L));
|
|
|
|
// if we couldn't get the function name, try populating using global table
|
|
if (fi.name.empty() && fi.what != "main")
|
|
populateFunctionName(L, fi);
|
|
|
|
result.push_back(fi);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
bool ScriptDebugger::onLineHook( lua_State* L, lua_Debug *ar )
|
|
{
|
|
if (currentLine > 0)
|
|
{
|
|
// Step over the current line. This prevents an infinite loop of breaks
|
|
lua_getinfo(L, "S", ar);
|
|
//RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Resuming: %s line %d", ar->short_src, ar->currentline);
|
|
currentLine = 0;
|
|
return false;
|
|
}
|
|
|
|
DebuggerBreakpoint* bp = NULL;
|
|
if (hasDifferentScriptInstances(L))
|
|
{
|
|
lua_getinfo(L, "f", ar);
|
|
RBX::Instance* scriptInstance = getScriptForLuaState(L);
|
|
if (scriptInstance)
|
|
{
|
|
if (ScriptDebugger* debugger = gDebuggerManager->findDebugger(scriptInstance))
|
|
bp = debugger->findBreakpoint(ar->currentline);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bp = findBreakpoint(ar->currentline);
|
|
}
|
|
|
|
if (bp && shouldBreak(bp, L))
|
|
{
|
|
debuggerBreak(L, ar);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ScriptDebugger::shouldBreak(DebuggerBreakpoint* bp, lua_State* L)
|
|
{
|
|
if (!bp->isEnabled())
|
|
return false;
|
|
|
|
if (bp->getCondition().empty())
|
|
return true;
|
|
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
|
|
lua_State* evalThread = lua_newthread(L);
|
|
RBXASSERT_BALLANCED_LUA_STACK2(evalThread);
|
|
|
|
std::string condition = "return " + bp->getCondition();
|
|
bool conditionFailedToLoad;
|
|
conditionFailedToLoad = LuaVM::load(evalThread, ProtectedString::fromTrustedSource(condition), "") != 0;
|
|
if (conditionFailedToLoad)
|
|
{
|
|
std::string err = Lua::safe_lua_tostring(evalThread, -1);
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Breakpoint %d condition syntax error: %s", bp->getLine(), err.c_str());
|
|
lua_pop(evalThread, 1); // pop the message
|
|
lua_pop(L, 1);
|
|
return false;
|
|
}
|
|
|
|
if (lua_pcall(evalThread, 0, 1, 0))
|
|
{
|
|
const char* message = lua_tostring(evalThread, -1);
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Breakpoint %d condition runtime error: %s", bp->getLine(), message);
|
|
lua_pop(evalThread, 1);
|
|
lua_pop(L, 1);
|
|
return false;
|
|
}
|
|
|
|
bool result = lua_toboolean(evalThread, -1) != 0;
|
|
lua_pop(evalThread, 1);
|
|
lua_pop(L, 1);
|
|
|
|
return result;
|
|
}
|
|
|
|
bool ScriptDebugger::askForbidChild(const Instance* newChild) const
|
|
{
|
|
if (Instance::fastDynamicCast<DebuggerWatch>(newChild))
|
|
return false;
|
|
|
|
if (Instance::fastDynamicCast<DebuggerBreakpoint>(newChild))
|
|
return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
void ScriptDebugger::verifyAddChild(const Instance* newChild) const
|
|
{
|
|
if (!askForbidChild(newChild))
|
|
return;
|
|
|
|
throw std::runtime_error("Only DebuggerWatch and DebuggerBreakpoint can be children of ScriptDebugger");
|
|
}
|
|
|
|
void ScriptDebugger::verifySetParent( const Instance* newParent ) const
|
|
{
|
|
if (newParent && !Instance::fastDynamicCast<DebuggerManager>(newParent))
|
|
throw std::runtime_error("ScriptDebugger must be a child of DebuggerManager");
|
|
}
|
|
|
|
shared_ptr<Instance> ScriptDebugger::setBreakpoint_Reflection( int line )
|
|
{
|
|
return setBreakpoint(line);
|
|
}
|
|
|
|
|
|
DebuggerBreakpoint* ScriptDebugger::findBreakpoint( int line )
|
|
{
|
|
if (!breakpoints.empty())
|
|
{
|
|
Breakpoints::const_iterator iter = breakpoints.find(line);
|
|
if (iter != breakpoints.end())
|
|
return (iter->second);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
shared_ptr<DebuggerBreakpoint> ScriptDebugger::setBreakpoint( int line )
|
|
{
|
|
if (DebuggerBreakpoint* breakpoint = findBreakpoint(line))
|
|
{
|
|
DebuggerBreakpoint::prop_Enabled.setValue(breakpoint, true);
|
|
return shared_from(breakpoint);
|
|
}
|
|
|
|
shared_ptr<DebuggerBreakpoint> breakpoint = Creatable<Instance>::create<DebuggerBreakpoint>(line);
|
|
breakpoint->setParent(this);
|
|
return breakpoint;
|
|
}
|
|
|
|
void ScriptDebugger::onChildRemoved( Instance* child )
|
|
{
|
|
if (DebuggerBreakpoint* breakpoint = Instance::fastDynamicCast<DebuggerBreakpoint>(child))
|
|
{
|
|
breakpoints.erase(breakpoint->getLine());
|
|
breakpointRemoved(shared_from(breakpoint));
|
|
}
|
|
if (DebuggerWatch* watch = Instance::fastDynamicCast<DebuggerWatch>(child))
|
|
{
|
|
watches.erase(std::remove(watches.begin(), watches.end(), watch), watches.end());
|
|
watchRemoved(shared_from(watch));
|
|
}
|
|
|
|
Super::onChildRemoved(child);
|
|
}
|
|
|
|
void ScriptDebugger::onChildAdded( Instance* child )
|
|
{
|
|
if (DebuggerBreakpoint* breakpoint = Instance::fastDynamicCast<DebuggerBreakpoint>(child))
|
|
{
|
|
RBXASSERT(breakpoint->getLine() > 0);
|
|
breakpoints[breakpoint->getLine()] = breakpoint;
|
|
breakpointAdded(shared_from(breakpoint));
|
|
}
|
|
if (DebuggerWatch* watch = Instance::fastDynamicCast<DebuggerWatch>(child))
|
|
{
|
|
watches.push_back(watch);
|
|
watchAdded(shared_from(watch));
|
|
}
|
|
|
|
updateHook();
|
|
Super::onChildAdded(child);
|
|
}
|
|
|
|
void ScriptDebugger::setScript( Instance* value )
|
|
{
|
|
if (Script* scriptPtr = Instance::fastDynamicCast<Script>(value))
|
|
{
|
|
setScript(scriptPtr);
|
|
}
|
|
else if (ModuleScript* moduleScriptPtr = Instance::fastDynamicCast<ModuleScript>(value))
|
|
{
|
|
setScript(moduleScriptPtr);
|
|
}
|
|
else
|
|
{
|
|
throw std::runtime_error("Debugger can be attached to a script or module script");
|
|
}
|
|
|
|
// add signal so we can clone debugger on script clone
|
|
scriptClonedConnection = value->onDemandWrite()->instanceClonedSignal.connect(boost::bind(&ScriptDebugger::onScriptCloned, this, _1));
|
|
}
|
|
|
|
void ScriptDebugger::setScript( Script* value )
|
|
{
|
|
if (!value)
|
|
throw std::runtime_error("Cannot attach debugger to a null script");
|
|
|
|
if (script.get() == value)
|
|
return;
|
|
|
|
RBXASSERT(!script); // Changing the script is not supported. DebuggerManager doesn't track changes like this
|
|
|
|
// check if we can get rootThread from threadNode
|
|
// NOTE: if script has been executed by the time this function is called then we will not have any root thread
|
|
// still debugger must be attached, so on reload of script debugging can take place
|
|
if (value->threadNode)
|
|
value->threadNode->forEachRefs(boost::bind(&ScriptDebugger::updateRootThread, this, _1));
|
|
|
|
scriptStartedConnection.disconnect();
|
|
scriptStoppedConnection.disconnect();
|
|
scriptParentChangedConnection.disconnect();
|
|
|
|
script = shared_from(value);
|
|
|
|
scriptStartedConnection = value->starting.connect(boost::bind(&ScriptDebugger::onScriptStarting, this, _1));
|
|
scriptStoppedConnection = value->stopped.connect(boost::bind(&ScriptDebugger::onScriptStopped, this));
|
|
scriptParentChangedConnection = value->ancestryChangedSignal.connect(boost::bind(&ScriptDebugger::onScriptParentChanged, this, _2));
|
|
|
|
raiseChanged(prop_Script);
|
|
raiseChanged(prop_ScriptPath);
|
|
}
|
|
|
|
void ScriptDebugger::setScript(ModuleScript* value)
|
|
{
|
|
if (!value)
|
|
throw std::runtime_error("Cannot attach debugger to a null script");
|
|
|
|
if (script.get() == value)
|
|
return;
|
|
|
|
RBXASSERT(!script); // Changing the script is not supported. DebuggerManager doesn't track changes like this
|
|
|
|
scriptParentChangedConnection.disconnect();
|
|
|
|
script = shared_from(value);
|
|
|
|
scriptStartedConnection = value->starting.connect(boost::bind(&ScriptDebugger::onScriptStarting, this, _1));
|
|
scriptParentChangedConnection = value->ancestryChangedSignal.connect(boost::bind(&ScriptDebugger::onScriptParentChanged, this, _2));
|
|
}
|
|
|
|
void ScriptDebugger::onScriptStarting( lua_State* L )
|
|
{
|
|
if (!FFlag::LuaDebugger)
|
|
return;
|
|
|
|
DebuggerManager& debuggerManager = RBX::Scripting::DebuggerManager::singleton();
|
|
if (!debuggerManager.getEnabled())
|
|
{
|
|
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING, "Can't debug script %s because debugging is disabled", script->getName().c_str());
|
|
return;
|
|
}
|
|
|
|
specialBreakpoint.reset();
|
|
rootThreadResumed = false;
|
|
currentThreadID = 0;
|
|
|
|
debuggerManager.populateForLookup(L, this);
|
|
|
|
lua_getfield(L, LUA_GLOBALSINDEX, "script");
|
|
globalRawScriptPtr = lua_touserdata(L, -1);
|
|
lua_pop(L, 1);
|
|
|
|
rootThread = L;
|
|
updateHook();
|
|
}
|
|
|
|
void ScriptDebugger::onScriptStopped()
|
|
{
|
|
specialBreakpoint.reset();
|
|
rootThread.reset();
|
|
pausedThread.reset();
|
|
errorThread.reset();
|
|
currentLine = 0;
|
|
breakpointHookData = NULL;
|
|
globalRawScriptPtr = NULL;
|
|
prevFuncTable = NULL;
|
|
prevRawScriptPtr = NULL;
|
|
rootThreadResumed = false;
|
|
currentThreadID = 0;
|
|
pausedThreads.clear();
|
|
}
|
|
|
|
void ScriptDebugger::onScriptParentChanged(shared_ptr<RBX::Instance> newParent)
|
|
{
|
|
if (!newParent)
|
|
{
|
|
// first remove all children
|
|
removeAllChildren();
|
|
// now reset debugger data
|
|
onScriptStopped();
|
|
// do the cleanup
|
|
setParent(NULL);
|
|
}
|
|
else
|
|
{
|
|
// else inform children about change in hierarchy
|
|
onAncestorChanged(AncestorChanged(this, script->getParent(), newParent.get()));
|
|
}
|
|
}
|
|
|
|
void ScriptDebugger::onScriptCloned(shared_ptr<RBX::Instance> clonedScript)
|
|
{
|
|
boost::shared_ptr<ScriptDebugger> clonedDebugger = createClone(clonedScript);
|
|
if (clonedDebugger)
|
|
RBX::Scripting::DebuggerManager::singleton().addDebugger(clonedDebugger);
|
|
}
|
|
|
|
bool ScriptDebugger::isPaused() const
|
|
{
|
|
return !pausedThreads.empty();
|
|
}
|
|
|
|
void ScriptDebugger::resume()
|
|
{
|
|
throw std::runtime_error("Depricated API - Use DebuggerManager::resume instead");
|
|
}
|
|
|
|
ScriptContext::Result ScriptDebugger::resumeThread(lua_State* thread, bool evalLineHookForCurrentLine)
|
|
{
|
|
ScriptContext::Result result(ScriptContext::Success);
|
|
// if we do not have paused thread then reset breakpoint data so we do not have incorrect execution halt when the thread resumes
|
|
if (!isPausedThread(reinterpret_cast<uintptr_t>(thread)) || isErrorThread(reinterpret_cast<uintptr_t>(thread)))
|
|
{
|
|
specialBreakpoint.reset();
|
|
breakpointHookData = NULL;
|
|
errorThread.reset();
|
|
pausedThreads.erase(reinterpret_cast<uintptr_t>(thread));
|
|
return result;
|
|
}
|
|
|
|
RBXASSERT_BALLANCED_LUA_STACK(thread);
|
|
pausedThread.reset();
|
|
pausedThreads.erase(reinterpret_cast<uintptr_t>(thread));
|
|
|
|
// if we need to evaluate line hook for current line then reset it to 0 (DE8670)
|
|
if (evalLineHookForCurrentLine)
|
|
currentLine = 0;
|
|
|
|
resuming();
|
|
if (breakpointHookData)
|
|
{
|
|
// We are in the breakpoint, so the yield will be aborted
|
|
}
|
|
else
|
|
{
|
|
updateHook();
|
|
result = ScriptContext::getContext(thread).resume(thread, 0);
|
|
if (!rootThreadResumed && isRootThread(reinterpret_cast<uintptr_t>(thread)) && (result == ScriptContext::Success))
|
|
rootThreadResumed = true;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool ScriptDebugger::isPausedThread(long threadID)
|
|
{
|
|
PausedThreads::iterator iter = pausedThreads.find(threadID);
|
|
return (iter != pausedThreads.end());
|
|
}
|
|
|
|
bool ScriptDebugger::isErrorThread(long threadID)
|
|
{
|
|
PausedThreads::iterator iter = pausedThreads.find(threadID);
|
|
if (iter != pausedThreads.end())
|
|
return iter->second.hasError;
|
|
return false;
|
|
}
|
|
|
|
bool ScriptDebugger::isRootThread(long threadID)
|
|
{
|
|
if (!rootThread.empty() && (threadID == reinterpret_cast<uintptr_t>( rootThread.lock().get())))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
shared_ptr<const Reflection::ValueMap> ScriptDebugger::getLocals(int stackIndex)
|
|
{
|
|
typedef shared_ptr<const Reflection::ValueMap> Result;
|
|
Result locals = withPausedThread<Result>(boost::bind(&readLocals, stackIndex, _1));
|
|
return locals;
|
|
}
|
|
|
|
shared_ptr<const Reflection::ValueMap> ScriptDebugger::getUpvalues(int stackIndex)
|
|
{
|
|
typedef shared_ptr<const Reflection::ValueMap> Result;
|
|
Result upvalues = withPausedThread<Result>(boost::bind(&readUpvalues, stackIndex, _1));
|
|
return upvalues;
|
|
}
|
|
shared_ptr<const Reflection::ValueMap> ScriptDebugger::getGlobals()
|
|
{
|
|
typedef shared_ptr<const Reflection::ValueMap> Result;
|
|
Result globals = withPausedThread<Result>(boost::bind(&readGlobals, _1));
|
|
return globals;
|
|
}
|
|
|
|
static bool doSetLocal(std::string valueName, const Reflection::Variant& value, int stackIndex, lua_State* L)
|
|
{
|
|
lua_Debug ar;
|
|
if (lua_getstack(L, stackIndex, &ar) == 0)
|
|
throw std::runtime_error("stackIndex out of range");
|
|
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
int n = 1;
|
|
bool foundIt = false;
|
|
while (const char* name = lua_getlocal(L, &ar, n))
|
|
{
|
|
lua_pop(L, 1); // pop value
|
|
if (valueName == name)
|
|
{
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
int count = Lua::LuaArguments::push(value, L);
|
|
RBXASSERT(count > 0);
|
|
const char* name2 = lua_setlocal(L, &ar, n);
|
|
RBXASSERT(strcmp(name, name2) == 0);
|
|
lua_pop(L, count-1);
|
|
foundIt = true;
|
|
break;
|
|
}
|
|
n++;
|
|
}
|
|
|
|
return foundIt;
|
|
}
|
|
|
|
void ScriptDebugger::setLocal( std::string name, Reflection::Variant value, int stackFrame)
|
|
{
|
|
if (!withPausedThread<bool>(boost::bind(&doSetLocal, name, boost::cref(value), stackFrame, _1)))
|
|
throw std::runtime_error(RBX::format("Bad local %s", name.c_str()));
|
|
}
|
|
|
|
static bool doSetUpvalue(std::string valueName, const Reflection::Variant& value, int stackIndex, lua_State* L)
|
|
{
|
|
lua_Debug ar;
|
|
if (lua_getstack(L, stackIndex, &ar) == 0)
|
|
throw std::runtime_error("stackIndex out of range");
|
|
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
int result = lua_getinfo(L, "fu", &ar);
|
|
RBXASSERT(result);
|
|
// stack contains current function
|
|
|
|
bool foundIt = false;
|
|
int n = 1;
|
|
while (const char* name = lua_getupvalue(L, -1, n))
|
|
{
|
|
lua_pop(L, 1); // pop value
|
|
if (valueName == name)
|
|
{
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
int count = Lua::LuaArguments::push(value, L);
|
|
RBXASSERT(count > 0);
|
|
lua_pop(L, count-1); // We only want 1 value on the stack (strip out tuples)
|
|
|
|
const char* name2 = lua_setupvalue(L, -2, n);
|
|
RBXASSERT(strcmp(name, name2) == 0);
|
|
foundIt = true;
|
|
break;
|
|
}
|
|
n++;
|
|
}
|
|
|
|
lua_pop(L, 1); // pop current function
|
|
|
|
return foundIt;
|
|
}
|
|
|
|
void ScriptDebugger::setUpvalue( std::string name, Reflection::Variant value, int stackFrame)
|
|
{
|
|
if (!withPausedThread<bool>(boost::bind(&doSetUpvalue, name, boost::cref(value), stackFrame, _1)))
|
|
throw std::runtime_error(RBX::format("Bad upvalue %s", name.c_str()));
|
|
}
|
|
|
|
|
|
static bool doSetGlobal(std::string valueName, const Reflection::Variant& value, lua_State* L)
|
|
{
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
|
|
int count = Lua::LuaArguments::push(value, L);
|
|
if (count == 0)
|
|
return false;
|
|
lua_setglobal(L, valueName.c_str());
|
|
lua_pop(L, count - 1);
|
|
return true;
|
|
}
|
|
|
|
void ScriptDebugger::setGlobal( std::string name, Reflection::Variant value)
|
|
{
|
|
if (!withPausedThread<bool>(boost::bind(&doSetGlobal, name, boost::cref(value), _1)))
|
|
throw std::runtime_error(RBX::format("Bad global %s", name.c_str()));
|
|
}
|
|
|
|
|
|
std::vector<ScriptDebugger::FunctionInfo> ScriptDebugger::getStack()
|
|
{
|
|
if (Lua::ThreadRef thread = pausedThread.lock())
|
|
return readStack(thread);
|
|
else
|
|
throw std::runtime_error("Cannot perform this operation unless the thread is paused");
|
|
}
|
|
|
|
shared_ptr<const Reflection::ValueArray> ScriptDebugger::getStack_Reflection()
|
|
{
|
|
shared_ptr<Reflection::ValueArray> result = rbx::make_shared<Reflection::ValueArray>();
|
|
|
|
Stack stack = getStack();
|
|
for (Stack::const_iterator iter = stack.begin(); iter != stack.end(); ++iter)
|
|
{
|
|
shared_ptr<Reflection::ValueMap> item = rbx::make_shared<Reflection::ValueMap>();
|
|
(*item)["frame"] = iter->frame;
|
|
(*item)["name"] = iter->name;
|
|
(*item)["currentline"] = iter->currentline;
|
|
(*item)["linedefined"] = iter->linedefined;
|
|
(*item)["lastlinedefined"] = iter->lastlinedefined;
|
|
(*item)["what"] = iter->what;
|
|
(*item)["namewhat"] = iter->namewhat;
|
|
(*item)["short_src"] = iter->short_src;
|
|
result->push_back(shared_ptr<const Reflection::ValueMap>(item));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void ScriptDebugger::pause()
|
|
{
|
|
if (isPaused())
|
|
return;
|
|
|
|
specialBreakpoint.reset(new PauseNowBreakpoint());
|
|
updateHook();
|
|
}
|
|
|
|
void ScriptDebugger::stepInto()
|
|
{
|
|
throw std::runtime_error("Depricated API - Use DebuggerManager::stepInto instead");
|
|
}
|
|
|
|
void ScriptDebugger::stepOut()
|
|
{
|
|
throw std::runtime_error("Depricated API - Use DebuggerManager::stepOut instead");
|
|
}
|
|
|
|
void ScriptDebugger::stepOver()
|
|
{
|
|
throw std::runtime_error("Depricated API - Use DebuggerManager::stepOver instead");
|
|
}
|
|
|
|
// Can only be called after lua_getinfo with "f" included in the parameters.
|
|
RBX::Instance* ScriptDebugger::getScriptForLuaState(lua_State* L)
|
|
{
|
|
RBXASSERT(lua_isnil(L, -1) || lua_isfunction(L, -1));
|
|
if (lua_isnil(L, -1) || lua_iscfunction(L, -1))
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
Lua::ScopedPopper pop(L, 2);
|
|
|
|
lua_getfenv(L, -1);
|
|
int fIndex = lua_gettop(L);
|
|
lua_getfield(L, fIndex, "script");
|
|
|
|
Reflection::Variant varResult;
|
|
Lua::LuaArguments::get(L, -1, varResult, false);
|
|
if (varResult.isType<boost::shared_ptr<RBX::Instance> >())
|
|
return varResult.get<boost::shared_ptr<RBX::Instance> >().get();
|
|
|
|
return NULL;
|
|
}
|
|
|
|
bool ScriptDebugger::hasDifferentScriptInstances(lua_State* L)
|
|
{
|
|
Closure* func = curr_func(L);
|
|
// verify if function environment is different
|
|
if (prevFuncTable != func->c.env)
|
|
{
|
|
// save env table
|
|
prevFuncTable = func->c.env;
|
|
|
|
// get "script" value
|
|
lua_getfield(L, LUA_ENVIRONINDEX, "script");
|
|
prevRawScriptPtr = lua_touserdata(L, -1);
|
|
lua_pop(L, 1);
|
|
|
|
// alternate way of accessing "script" field (it uses internal lua calls)
|
|
//prevRawScriptPtr = rawuvalue(luaH_getstr(prevFuncTable, luaS_new(L, "script"))) + 1;
|
|
}
|
|
|
|
return (globalRawScriptPtr != prevRawScriptPtr);
|
|
}
|
|
|
|
static std::string getHashString(RBX::Instance* pInstance)
|
|
{
|
|
if (Script* scriptPtr = Instance::fastDynamicCast<Script>(pInstance))
|
|
return scriptPtr->requestHash();
|
|
|
|
if (ModuleScript* moduleScriptPtr = Instance::fastDynamicCast<ModuleScript>(pInstance))
|
|
return moduleScriptPtr->requestHash();
|
|
|
|
return std::string();
|
|
}
|
|
|
|
//script path - indices of the children hirearchy upto datamodel
|
|
/*
|
|
Following is an example, showing generated script path
|
|
|
|
DataModel
|
|
Workspace
|
|
Script*
|
|
Model
|
|
Part
|
|
Part
|
|
Script**
|
|
Model
|
|
Model
|
|
Script***
|
|
Script
|
|
Script****
|
|
|
|
Script* : 0|0 (1st child of datamodel --> 1st child of workspace)
|
|
Script** : 0|1|2
|
|
Script*** : 0|2|0|0
|
|
Script**** : 0|2|1
|
|
|
|
With Hash string,
|
|
Script* : 0|0-Hash_String
|
|
Script** : 0|1|2-Hash_String
|
|
Script*** : 0|2|0|0-Hash_String
|
|
Script**** : 0|2|1-Hash_String
|
|
|
|
Scripts will be retrieved using datamodel and the hiereachy saved.
|
|
*/
|
|
std::string ScriptDebugger::getScriptPath() const
|
|
{
|
|
if (!script)
|
|
return "";
|
|
|
|
const RBX::Instance* pParent = script->getParent();
|
|
const RBX::Instance* pChild = script.get();
|
|
|
|
std::vector<int> childIndices;
|
|
while (pParent)
|
|
{
|
|
childIndices.push_back(pParent->findChildIndex(pChild));
|
|
|
|
pChild = pParent;
|
|
pParent = pChild->getParent();
|
|
}
|
|
|
|
if (!childIndices.size())
|
|
return "";
|
|
|
|
std::ostringstream ss;
|
|
std::copy(childIndices.rbegin(), childIndices.rend() - 1, std::ostream_iterator<int>(ss, "|"));
|
|
ss << childIndices.front();
|
|
|
|
std::string result(ss.str());
|
|
result.append("-");
|
|
result.append(getHashString(script.get()));
|
|
return result;
|
|
}
|
|
|
|
void ScriptDebugger::setScriptPath(std::string scriptPath)
|
|
{
|
|
RBX::Instance* pParent = RBX::DataModel::get(RBX::Scripting::DebuggerManager::singleton().getDataModel());
|
|
RBX::Instance* pChild = NULL;
|
|
|
|
std::string childPath(scriptPath), savedHashString;
|
|
|
|
std::vector<std::string> strings;
|
|
boost::split(strings, scriptPath, boost::is_any_of("-"));
|
|
|
|
// we must have 2 strings separated by "-"
|
|
if (strings.size() != 2)
|
|
throw std::runtime_error("Debugger data cannot be loaded");
|
|
|
|
childPath = strings[0];
|
|
savedHashString = strings[1];
|
|
|
|
int childNum = 0;
|
|
boost::tokenizer<boost::char_separator<char> > tokens(childPath, boost::char_separator<char>("|"));
|
|
for (boost::tokenizer<boost::char_separator<char> > ::iterator tok_iter = tokens.begin(); tok_iter != tokens.end(); ++tok_iter)
|
|
{
|
|
if (!pParent)
|
|
throw std::runtime_error("Debugger data cannot be loaded");
|
|
|
|
//get the child from index
|
|
childNum = atoi((*tok_iter).c_str());
|
|
if (childNum < 0 || childNum >= (int)pParent->numChildren())
|
|
throw std::runtime_error("Debugger data cannot be loaded");
|
|
|
|
pChild = pParent->getChild(childNum);
|
|
pParent = pChild;
|
|
}
|
|
|
|
if (getHashString(pChild) != savedHashString)
|
|
throw std::runtime_error("Debugger data cannot be loaded");
|
|
|
|
setScript(pChild);
|
|
}
|
|
|
|
void ScriptDebugger::updateRootThread(ScriptDebugger* scriptDebugger, lua_State *L)
|
|
{
|
|
// set root thread if it's not there
|
|
if (L && scriptDebugger && scriptDebugger->rootThread.empty())
|
|
scriptDebugger->rootThread = L;
|
|
|
|
}
|
|
|
|
void ScriptDebugger::setLuaHook(ScriptDebugger* scriptDebugger, int hookMask, lua_State *L)
|
|
{
|
|
if (L && (L->hookmask != hookMask))
|
|
{
|
|
const RobloxExtraSpace* pExtraSpace = RobloxExtraSpace::get(L);
|
|
if (pExtraSpace)
|
|
{
|
|
shared_ptr<BaseScript> script = pExtraSpace->script.lock();
|
|
if (script && (scriptDebugger->script == script))
|
|
{
|
|
lua_sethook(L, DebuggerManager::hook, hookMask, ScriptContext::hookCount);
|
|
}
|
|
else
|
|
{
|
|
shared_ptr<RBX::ModuleScript> pModuleScript = RBX::Instance::fastSharedDynamicCast<RBX::ModuleScript>(scriptDebugger->script);
|
|
if (pModuleScript)
|
|
lua_sethook(L, DebuggerManager::hook, hookMask, ScriptContext::hookCount);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptDebugger::updateHook()
|
|
{
|
|
if (!script)
|
|
return;
|
|
|
|
if (rootThread.empty())
|
|
{
|
|
Script* scriptPtr = Instance::fastDynamicCast<Script>(script.get());
|
|
if (!scriptPtr || !scriptPtr->threadNode)
|
|
return;
|
|
|
|
// get root thread from threadNode
|
|
scriptPtr->threadNode->forEachRefs(boost::bind(&ScriptDebugger::updateRootThread, this, _1));
|
|
if (rootThread.empty())
|
|
return;
|
|
}
|
|
|
|
// set hook for all the dependent threads
|
|
RobloxExtraSpace* pExtraSpace = RobloxExtraSpace::get(rootThread.lock());
|
|
if (pExtraSpace)
|
|
{
|
|
int hookMask = LUA_MASKLINE | LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT;
|
|
if (RBX::Scripting::DebuggerManager::singleton().getBreakOnErrorMode() == BreakOnErrorMode_AllExceptions)
|
|
hookMask = LUA_MASKLINE | LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT | LUA_MASKERROR;
|
|
pExtraSpace->forEachThread(boost::bind(&ScriptDebugger::setLuaHook, this, hookMask, _1));
|
|
}
|
|
}
|
|
|
|
void ScriptDebugger::handleError(lua_State* thread)
|
|
{
|
|
if (!FFlag::LuaDebuggerBreakOnError)
|
|
return;
|
|
|
|
// save thread
|
|
errorThread = thread;
|
|
|
|
// get error message and read stack information
|
|
std::string errorMessage = RBX::Lua::safe_lua_tostring(thread, -1);
|
|
Stack stack = readStack(thread);
|
|
|
|
// cleanup stack (for a C function currentline can be -1, so we cannot break there)
|
|
if (stack.size() > 0 && stack[0].currentline < 0)
|
|
stack.erase(stack.begin());
|
|
|
|
handleError(errorMessage, stack);
|
|
}
|
|
|
|
void ScriptDebugger::handleError(std::string errorMessage, const Stack& stack)
|
|
{
|
|
if (!FFlag::LuaDebuggerBreakOnError)
|
|
return;
|
|
|
|
// for syntax error related errors, we need to extract line number from message itself
|
|
if (!errorMessage.empty())
|
|
{
|
|
size_t start = errorMessage.find(':');
|
|
size_t stop = errorMessage.find(':', start + 1);
|
|
if (stop > start)
|
|
{
|
|
currentLine = atoi(errorMessage.substr(start + 1, stop).c_str());
|
|
errorMessage = errorMessage.substr(stop + 1);
|
|
}
|
|
}
|
|
|
|
if (stack.size() > 0)
|
|
currentLine = stack[0].currentline;
|
|
|
|
// we must have a valid error line
|
|
if (currentLine > 0)
|
|
{
|
|
if (!errorThread.empty())
|
|
{
|
|
PausedThreadData data;
|
|
|
|
data.pausedLine = currentLine;
|
|
data.callStack = stack;
|
|
|
|
data.hasError = true;
|
|
data.errorMessage = errorMessage;
|
|
data.thread = errorThread;
|
|
|
|
long threadID = reinterpret_cast<uintptr_t>(errorThread.lock().get());
|
|
pausedThreads[threadID] = data;
|
|
currentThreadID = threadID;
|
|
|
|
scriptErrorDetected(currentLine, errorMessage, stack);
|
|
}
|
|
else
|
|
{
|
|
scriptErrorDetected(currentLine, errorMessage, stack);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptDebugger::setCurrentThread(long threadID)
|
|
{
|
|
PausedThreads::iterator iter = pausedThreads.find(threadID);
|
|
if (iter != pausedThreads.end())
|
|
{
|
|
currentThreadID = threadID;
|
|
|
|
PausedThreadData data = iter->second;
|
|
currentLine = data.pausedLine;
|
|
data.hasError ? errorThread = data.thread : pausedThread = data.thread;
|
|
}
|
|
}
|
|
|
|
boost::shared_ptr<ScriptDebugger> ScriptDebugger::createClone(boost::shared_ptr<Instance> clonedScript)
|
|
{
|
|
// first create debugger
|
|
boost::shared_ptr<ScriptDebugger> clonedDebugger = Creatable<Instance>::create<ScriptDebugger>(boost::ref(*clonedScript));
|
|
|
|
// now it's children
|
|
const Scripting::ScriptDebugger::Breakpoints& breakpoints = getBreakpoints();
|
|
for (RBX::Scripting::ScriptDebugger::Breakpoints::const_iterator iter = breakpoints.begin(); iter != breakpoints.end(); ++iter)
|
|
{
|
|
if (iter->second)
|
|
{
|
|
shared_ptr<Instance> breakpoint = iter->second->clone(RBX::EngineCreator);
|
|
if (breakpoint)
|
|
breakpoint->setParent(clonedDebugger.get());
|
|
}
|
|
}
|
|
|
|
const Scripting::ScriptDebugger::Watches& watches = getWatches();
|
|
for (RBX::Scripting::ScriptDebugger::Watches::const_iterator iter = watches.begin(); iter != watches.end(); ++iter)
|
|
{
|
|
if (*iter)
|
|
{
|
|
shared_ptr<Instance> watch = (*iter)->clone(RBX::EngineCreator);
|
|
if (watch)
|
|
watch->setParent(clonedDebugger.get());
|
|
}
|
|
}
|
|
|
|
return clonedDebugger;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// DebuggerBreakpoint
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
DebuggerBreakpoint::DebuggerBreakpoint(void)
|
|
:enabled(true)
|
|
,line(0)
|
|
{
|
|
}
|
|
|
|
DebuggerBreakpoint::DebuggerBreakpoint( int line )
|
|
:enabled(true)
|
|
,line(line)
|
|
{
|
|
RBXASSERT(line > 0);
|
|
setName(RBX::format("Breakpoint%d", line));
|
|
}
|
|
|
|
DebuggerBreakpoint::~DebuggerBreakpoint(void)
|
|
{
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// DebuggerWatch
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
DebuggerWatch::DebuggerWatch(std::string expression)
|
|
:expression(expression)
|
|
{
|
|
setName(sDebuggerWatch);
|
|
}
|
|
|
|
void DebuggerBreakpoint::verifySetParent( const Instance* newParent ) const
|
|
{
|
|
if (newParent && !Instance::fastDynamicCast<ScriptDebugger>(newParent))
|
|
throw std::runtime_error("Breakpoints must be a child of ScriptDebugger");
|
|
}
|
|
|
|
void DebuggerWatch::verifySetParent( const Instance* newParent ) const
|
|
{
|
|
if (newParent && !Instance::fastDynamicCast<ScriptDebugger>(newParent))
|
|
throw std::runtime_error("DebuggerWatch must be a child of ScriptDebugger");
|
|
}
|
|
|
|
|
|
void DebuggerWatch::checkExpressionSyntax()
|
|
{
|
|
Lua::ScopedState L;
|
|
RBXASSERT_BALLANCED_LUA_STACK(L);
|
|
|
|
const std::string code = "return " + expression;
|
|
bool error;
|
|
error = LuaVM::load(L, ProtectedString::fromTrustedSource(code), "") == LUA_ERRSYNTAX;
|
|
Lua::ScopedPopper pop(L, 1);
|
|
if (error)
|
|
{
|
|
std::string err = Lua::safe_lua_tostring(L, -1);
|
|
throw RBX::runtime_error("syntax error: %s", err.c_str());
|
|
}
|
|
}
|