#include "pch.h" #include #include #include "Lua/lua.hpp" #include "Lua/luasocket.hpp" #include "Debugger/ScriptingContext.h" #include "Debugger/LuaApi.h" #include "Debugger/LuaCallHelper.h" #include "Debugger/DebugTypes.h" #include "Debugger/Debugger.h" #include "Debugger/ScriptManager.h" #include "Shared/Emulator.h" #include "Shared/EmuSettings.h" #include "Shared/EventType.h" #include "Utilities/magic_enum.hpp" #include "Utilities/StringUtilities.h" ScriptingContext* ScriptingContext::_context = nullptr; ScriptingContext::ScriptingContext(Debugger* debugger) { _debugger = debugger; _settings = debugger->GetEmulator()->GetSettings(); _defaultCpuType = debugger->GetEmulator()->GetCpuTypes()[0]; _defaultMemType = DebugUtilities::GetCpuMemoryType(_defaultCpuType); } ScriptingContext::~ScriptingContext() { if(_lua) { //Cleanup all references, this is required to prevent crashes that can occur when calling lua_close std::unordered_set references; for(int i = (int)CallbackType::Read; i <= (int)CallbackType::Exec; i++) { for(MemoryCallback& callback : _callbacks[i]) { references.emplace(callback.Reference); } } for(auto& entry : magic_enum::enum_entries()) { for(int& ref : _eventCallbacks[(int)entry.first]) { references.emplace(ref); } } for(const int& ref : references) { luaL_unref(_lua, LUA_REGISTRYINDEX, ref); } lua_close(_lua); _lua = nullptr; } } bool ScriptingContext::LoadScript(string scriptName, string path, string scriptContent, Debugger* debugger) { _scriptName = scriptName; int iErr = 0; _lua = luaL_newstate(); _context = this; LuaApi::SetContext(this); EmuSettings* settings = debugger->GetEmulator()->GetSettings(); bool allowIoOsAccess = settings->GetDebugConfig().ScriptAllowIoOsAccess; LuaOpenLibs(_lua, allowIoOsAccess); //Prevent lua code from loading any files SANDBOX_ALLOW_LOADFILE = allowIoOsAccess ? 1 : 0; //Load LuaSocket into Lua core if(allowIoOsAccess && settings->GetDebugConfig().ScriptAllowNetworkAccess) { lua_getglobal(_lua, "package"); lua_getfield(_lua, -1, "preload"); lua_pushcfunction(_lua, luaopen_socket_core); lua_setfield(_lua, -2, "socket.core"); lua_pushcfunction(_lua, luaopen_mime_core); lua_setfield(_lua, -2, "mime.core"); lua_pop(_lua, 2); } if(allowIoOsAccess) { //Escape backslashes std::regex r("\\\\"); path = std::regex_replace(path, r, "\\\\"); //Add path for the current Lua script to package.path to allow //using require() without specifying an absolute path, etc. string cmd = "package.path = package.path .. ';" + path + "?.lua'"; luaL_dostring(_lua, cmd.c_str()); } luaL_requiref(_lua, "emu", LuaApi::GetLibrary, 1); Log("Loading script..."); if((iErr = luaL_loadbufferx(_lua, scriptContent.c_str(), scriptContent.size(), ("@" + scriptName).c_str(), nullptr)) == 0) { _timer.Reset(); lua_setwatchdogtimer(_lua, ScriptingContext::ExecutionCountHook, 1000); if((iErr = lua_pcall(_lua, 0, LUA_MULTRET, 0)) == 0) { //Script loaded properly Log("Script loaded successfully."); _initDone = true; return true; } } ProcessLuaError(); return false; } string ScriptingContext::GetErrorMessage() { string errorMsg = ""; if(lua_type(_lua, -1) == LUA_TSTRING) { errorMsg = lua_tostring(_lua, -1); } else if(lua_istable(_lua, -1)) { //Serialize tables to Lua-formatted string errorMsg = "error: " + LuaApi::SerializeTable(_lua); } else { //Convert all other values to a string luaL_tolstring(_lua, -1, nullptr); errorMsg = string("error: ") + lua_tostring(_lua, -1); lua_pop(_lua, 1); } return errorMsg; } void ScriptingContext::ProcessLuaError() { string errorMsg = GetErrorMessage(); if(StringUtilities::Contains(errorMsg, "attempt to call a nil value (global 'require')") || StringUtilities::Contains(errorMsg, "attempt to index a nil value (global 'os')") || StringUtilities::Contains(errorMsg, "attempt to index a nil value (global 'io')")) { Log("I/O and OS libraries are disabled by default for security.\nYou can enable them here:\nScript->Settings->Script Window->Restrictions->Allow access to I/O and OS functions."); } else if(StringUtilities::Contains(errorMsg, "module 'socket.core' not found")) { Log("Lua sockets are disabled by default for security.\nYou can enable them here:\nScript->Settings->Script Window->Restrictions->Allow network access."); } else { Log(errorMsg); } lua_pop(_lua, 1); } void ScriptingContext::ExecutionCountHook(lua_State* lua) { uint32_t timeout = _context->_settings->GetDebugConfig().ScriptTimeout; if(_context->_timer.GetElapsedMS() > timeout * 1000) { luaL_error(lua, (std::string("Maximum execution time (") + std::to_string(timeout) + " seconds) exceeded.").c_str()); } lua_setwatchdogtimer(lua, ScriptingContext::ExecutionCountHook, 1000); } void ScriptingContext::LuaOpenLibs(lua_State* L, bool allowIoOsAccess) { constexpr luaL_Reg loadedlibs[] = { { "_G", luaopen_base }, { LUA_LOADLIBNAME, luaopen_package }, { LUA_COLIBNAME, luaopen_coroutine }, { LUA_TABLIBNAME, luaopen_table }, { LUA_IOLIBNAME, luaopen_io }, { LUA_OSLIBNAME, luaopen_os }, { LUA_STRLIBNAME, luaopen_string }, { LUA_MATHLIBNAME, luaopen_math }, { LUA_UTF8LIBNAME, luaopen_utf8 }, { LUA_DBLIBNAME, luaopen_debug }, { NULL, NULL } }; const luaL_Reg* lib; /* "require" functions from 'loadedlibs' and set results to global table */ for(lib = loadedlibs; lib->func; lib++) { if(!allowIoOsAccess) { //Skip loading IO, OS and Package lib when sandboxed if(strcmp(lib->name, LUA_IOLIBNAME) == 0 || strcmp(lib->name, LUA_OSLIBNAME) == 0 || strcmp(lib->name, LUA_LOADLIBNAME) == 0) { continue; } } luaL_requiref(L, lib->name, lib->func, 1); lua_pop(L, 1); /* remove lib */ } } void ScriptingContext::Log(string message) { auto lock = _logLock.AcquireSafe(); size_t start = 0; if(message.size() <= 200) { _logRows.push_back(message); if(_logRows.size() > 500) { _logRows.pop_front(); } } else { //Split large strings into several separate lines //This is needed to prevent performance issues in the UI //when a single line is several thousand characters long. while(start < message.size()) { size_t pos = message.find_first_of(',', start + 200); if(pos == string::npos) { pos = message.find_first_of(' ', start + 200); if(pos == string::npos) { pos = std::min(message.size() - 1, start + 300); } } _logRows.push_back(message.substr(start, pos - start + 1)); start = pos + 1; if(_logRows.size() > 500) { _logRows.pop_front(); } } } } string ScriptingContext::GetLog() { auto lock = _logLock.AcquireSafe(); stringstream ss; for(string& msg : _logRows) { ss << msg << "\n"; } return ss.str(); } Debugger* ScriptingContext::GetDebugger() { return _debugger; } string ScriptingContext::GetScriptName() { return _scriptName; } template void ScriptingContext::CallMemoryCallback(AddressInfo relAddr, T& value, CallbackType type, CpuType cpuType) { _allowSaveState = type == CallbackType::Exec && cpuType == _defaultCpuType; InternalCallMemoryCallback(relAddr, value, type, cpuType); _allowSaveState = false; } bool ScriptingContext::CheckInitDone() { return _initDone; } bool ScriptingContext::IsSaveStateAllowed() { return _allowSaveState; } void ScriptingContext::RegisterMemoryCallback(CallbackType type, int startAddr, int endAddr, MemoryType memType, CpuType cpuType, int reference) { if(endAddr < startAddr) { return; } MemoryCallback callback; callback.StartAddress = (uint32_t)startAddr; callback.EndAddress = (uint32_t)endAddr; callback.Reference = reference; callback.Cpu = cpuType; callback.MemType = memType; if(DebugUtilities::IsPpuMemory(memType)) { _debugger->GetScriptManager()->EnablePpuMemoryCallbacks(); } else { _debugger->GetScriptManager()->EnableCpuMemoryCallbacks(); } _callbacks[(int)type].push_back(callback); } void ScriptingContext::RefreshMemoryCallbackFlags() { for(int i = (int)CallbackType::Read; i <= (int)CallbackType::Exec; i++) { for(size_t j = 0, len = _callbacks[i].size(); j < len; j++) { if(DebugUtilities::IsPpuMemory(_callbacks[i][j].MemType)) { _debugger->GetScriptManager()->EnablePpuMemoryCallbacks(); } else { _debugger->GetScriptManager()->EnableCpuMemoryCallbacks(); } } } } void ScriptingContext::UnregisterMemoryCallback(CallbackType type, int startAddr, int endAddr, MemoryType memType, CpuType cpuType, int reference) { if(endAddr < startAddr) { return; } for(size_t i = 0; i < _callbacks[(int)type].size(); i++) { MemoryCallback& callback = _callbacks[(int)type][i]; bool isMatch = callback.Reference == reference && callback.Cpu == cpuType && callback.MemType == memType && (int)callback.StartAddress == startAddr && (int)callback.EndAddress == endAddr; if(isMatch) { _callbacks[(int)type].erase(_callbacks[(int)type].begin() + i); break; } } luaL_unref(_lua, LUA_REGISTRYINDEX, reference); } void ScriptingContext::RegisterEventCallback(EventType type, int reference) { _eventCallbacks[(int)type].push_back(reference); } void ScriptingContext::UnregisterEventCallback(EventType type, int reference) { vector& callbacks = _eventCallbacks[(int)type]; callbacks.erase(std::remove(callbacks.begin(), callbacks.end(), reference), callbacks.end()); luaL_unref(_lua, LUA_REGISTRYINDEX, reference); } bool ScriptingContext::IsAddressMatch(MemoryCallback& callback, AddressInfo addr) { return addr.Type == callback.MemType && addr.Address >= (int32_t)callback.StartAddress && addr.Address <= (int32_t)callback.EndAddress; } template void ScriptingContext::InternalCallMemoryCallback(AddressInfo relAddr, T& value, CallbackType type, CpuType cpuType) { if(_callbacks[(int)type].empty()) { return; } _context = this; bool needTimerReset = true; lua_setwatchdogtimer(_lua, ScriptingContext::ExecutionCountHook, 1000); LuaApi::SetContext(this); for(MemoryCallback& callback : _callbacks[(int)type]) { if(callback.Cpu != cpuType) { continue; } int32_t address; if(DebugUtilities::IsRelativeMemory(callback.MemType)) { if(!IsAddressMatch(callback, relAddr)) { continue; } address = relAddr.Address; } else { AddressInfo absAddr = _debugger->GetAbsoluteAddress(relAddr); if(!IsAddressMatch(callback, absAddr)) { continue; } address = absAddr.Address; } if(needTimerReset) { _timer.Reset(); needTimerReset = false; } int top = lua_gettop(_lua); lua_rawgeti(_lua, LUA_REGISTRYINDEX, callback.Reference); lua_pushinteger(_lua, address); lua_pushinteger(_lua, value); if(lua_pcall(_lua, 2, LUA_MULTRET, 0) != 0) { ProcessLuaError(); } else { int returnParamCount = lua_gettop(_lua) - top; if(returnParamCount && lua_isinteger(_lua, -1)) { int newValue = (int)lua_tointeger(_lua, -1); value = (T)newValue; } lua_settop(_lua, top); } } } int ScriptingContext::CallEventCallback(EventType type, CpuType cpuType) { if(_eventCallbacks[(int)type].empty()) { return 0; } _timer.Reset(); _context = this; lua_setwatchdogtimer(_lua, ScriptingContext::ExecutionCountHook, 1000); LuaApi::SetContext(this); LuaCallHelper l(_lua); for(int& ref : _eventCallbacks[(int)type]) { lua_rawgeti(_lua, LUA_REGISTRYINDEX, ref); lua_pushinteger(_lua, (int)cpuType); if(lua_pcall(_lua, 1, 0, 0) != 0) { ProcessLuaError(); } } return l.ReturnCount(); } template void ScriptingContext::CallMemoryCallback(AddressInfo relAddr, uint8_t& value, CallbackType type, CpuType cpuType); template void ScriptingContext::CallMemoryCallback(AddressInfo relAddr, uint16_t& value, CallbackType type, CpuType cpuType); template void ScriptingContext::CallMemoryCallback(AddressInfo relAddr, uint32_t& value, CallbackType type, CpuType cpuType);