Added JS script monitoring thread to prevent bad scripts from hanging the gui.

This commit is contained in:
harry 2024-02-04 22:09:59 -05:00
parent 5495c7eddc
commit 1fc813803e
2 changed files with 211 additions and 48 deletions

View File

@ -131,12 +131,12 @@ bool EmuScriptObject::paused()
return FCEUI_EmulationPaused() != 0;
}
//----------------------------------------------------
int EmuScriptObject::framecount()
int EmuScriptObject::frameCount()
{
return FCEUMOV_GetFrame();
}
//----------------------------------------------------
int EmuScriptObject::lagcount()
int EmuScriptObject::lagCount()
{
return FCEUI_GetLagCount();
}
@ -146,7 +146,7 @@ bool EmuScriptObject::lagged()
return FCEUI_GetLagged();
}
//----------------------------------------------------
void EmuScriptObject::setlagflag(bool flag)
void EmuScriptObject::setLagFlag(bool flag)
{
FCEUI_SetLagFlag(flag);
}
@ -200,14 +200,14 @@ void EmuScriptObject::speedMode(const QString& mode)
FCEUD_SetEmulationSpeed(speed);
}
//----------------------------------------------------
void EmuScriptObject::registerBefore(const QJSValue& func)
void EmuScriptObject::registerBeforeFrame(const QJSValue& func)
{
script->registerBefore(func);
script->registerBeforeEmuFrame(func);
}
//----------------------------------------------------
void EmuScriptObject::registerAfter(const QJSValue& func)
void EmuScriptObject::registerAfterFrame(const QJSValue& func)
{
script->registerAfter(func);
script->registerAfterEmuFrame(func);
}
//----------------------------------------------------
void EmuScriptObject::registerStop(const QJSValue& func)
@ -278,7 +278,7 @@ static void addressReadCallback(unsigned int address, unsigned int value, void *
{
QJSValueList args = { address, value };
func->call(args);
mem->getScript()->runFunc( *func, args);
}
}
}
@ -295,7 +295,7 @@ static void addressWriteCallback(unsigned int address, unsigned int value, void
{
QJSValueList args = { address, value };
func->call(args);
mem->getScript()->runFunc( *func, args);
}
}
}
@ -312,7 +312,7 @@ static void addressExecCallback(unsigned int address, unsigned int value, void *
{
QJSValueList args = { address, value };
func->call(args);
mem->getScript()->runFunc( *func, args);
}
}
}
@ -699,6 +699,11 @@ void QtScriptInstance::shutdownEngine()
delete onScriptStopCallback;
onScriptStopCallback = nullptr;
}
if (onGuiUpdateCallback != nullptr)
{
delete onGuiUpdateCallback;
onGuiUpdateCallback = nullptr;
}
if (engine != nullptr)
{
@ -849,7 +854,7 @@ void QtScriptInstance::loadUI(const QString& uiFilePath)
#endif
}
//----------------------------------------------------
void QtScriptInstance::registerBefore(const QJSValue& func)
void QtScriptInstance::registerBeforeEmuFrame(const QJSValue& func)
{
if (onFrameBeginCallback != nullptr)
{
@ -858,7 +863,7 @@ void QtScriptInstance::registerBefore(const QJSValue& func)
onFrameBeginCallback = new QJSValue(func);
}
//----------------------------------------------------
void QtScriptInstance::registerAfter(const QJSValue& func)
void QtScriptInstance::registerAfterEmuFrame(const QJSValue& func)
{
if (onFrameFinishCallback != nullptr)
{
@ -876,6 +881,15 @@ void QtScriptInstance::registerStop(const QJSValue& func)
onScriptStopCallback = new QJSValue(func);
}
//----------------------------------------------------
void QtScriptInstance::registerGuiUpdate(const QJSValue& func)
{
if (onGuiUpdateCallback != nullptr)
{
delete onGuiUpdateCallback;
}
onGuiUpdateCallback = new QJSValue(func);
}
//----------------------------------------------------
void QtScriptInstance::print(const QString& msg)
{
if (dialog)
@ -933,6 +947,26 @@ void QtScriptInstance::printSymbols(QJSValue& val, int iter)
}
}
//----------------------------------------------------
int QtScriptInstance::runFunc(QJSValue &func, const QJSValueList& args)
{
int retval = 0;
auto state = getExecutionState();
state->start();
QJSValue callResult = func.call(args);
state->stop();
if (callResult.isError())
{
retval = -1;
running = false;
print(callResult.toString());
}
return retval;
}
//----------------------------------------------------
int QtScriptInstance::call(const QString& funcName, const QJSValueList& args)
{
if (engine == nullptr)
@ -947,19 +981,10 @@ int QtScriptInstance::call(const QString& funcName, const QJSValueList& args)
QJSValue func = engine->globalObject().property(funcName);
FCEU_WRAPPER_LOCK();
QJSValue callResult = func.call(args);
int retval = runFunc(func, args);
FCEU_WRAPPER_UNLOCK();
if (callResult.isError())
{
print(callResult.toString());
}
else
{
//printf("Script Call Success!\n");
}
return 0;
return retval;
}
//----------------------------------------------------
void QtScriptInstance::stopRunning()
@ -969,12 +994,7 @@ void QtScriptInstance::stopRunning()
{
if (onScriptStopCallback != nullptr && onScriptStopCallback->isCallable())
{
QJSValue callResult = onScriptStopCallback->call();
if (callResult.isError())
{
print(callResult.toString());
}
runFunc( *onScriptStopCallback );
}
running = false;
@ -987,13 +1007,7 @@ void QtScriptInstance::onFrameBegin()
{
if (running && onFrameBeginCallback != nullptr && onFrameBeginCallback->isCallable())
{
QJSValue callResult = onFrameBeginCallback->call();
if (callResult.isError())
{
print(callResult.toString());
running = false;
}
runFunc( *onFrameBeginCallback );
}
}
//----------------------------------------------------
@ -1001,14 +1015,59 @@ void QtScriptInstance::onFrameFinish()
{
if (running && onFrameFinishCallback != nullptr && onFrameFinishCallback->isCallable())
{
QJSValue callResult = onFrameFinishCallback->call();
runFunc( *onFrameFinishCallback );
}
}
//----------------------------------------------------
void QtScriptInstance::onGuiUpdate()
{
if (running && onGuiUpdateCallback != nullptr && onGuiUpdateCallback->isCallable())
{
runFunc( *onGuiUpdateCallback );
}
}
//----------------------------------------------------
ScriptExecutionState* QtScriptInstance::getExecutionState()
{
ScriptExecutionState* state;
if (callResult.isError())
if (onEmulationThread())
{
state = &emuFuncState;
}
else
{
state = &guiFuncState;
}
return state;
}
//----------------------------------------------------
void QtScriptInstance::checkForHang()
{
static constexpr uint64_t funcTimeoutMs = 1000;
if ( guiFuncState.isRunning() )
{
uint64_t timeRunningMs = guiFuncState.timeRunning();
if (timeRunningMs > funcTimeoutMs)
{
print(callResult.toString());
running = false;
printf("Interrupted GUI Thread Script Function\n");
engine->setInterrupted(true);
}
}
if ( emuFuncState.isRunning() )
{
uint64_t timeRunningMs = emuFuncState.timeRunning();
if (timeRunningMs > funcTimeoutMs)
{
printf("Interrupted Emulation Thread Script Function\n");
engine->setInterrupted(true);
}
}
}
//----------------------------------------------------
QString QtScriptInstance::openFileBrowser(const QString& initialPath)
@ -1069,10 +1128,19 @@ QtScriptManager::QtScriptManager(QObject* parent)
: QObject(parent)
{
_instance = this;
monitorThread = new ScriptMonitorThread_t();
monitorThread->start();
periodicUpdateTimer = new QTimer(this);
connect( periodicUpdateTimer, &QTimer::timeout, this, &QtScriptManager::guiUpdate );
periodicUpdateTimer->start(50); // ~20hz
}
//----------------------------------------------------
QtScriptManager::~QtScriptManager()
{
monitorThread->requestInterruption();
monitorThread->wait();
_instance = nullptr;
//printf("QtScriptManager destroyed\n");
}
@ -1096,11 +1164,13 @@ void QtScriptManager::destroy(void)
//----------------------------------------------------
void QtScriptManager::addScriptInstance(QtScriptInstance* script)
{
FCEU::autoScopedLock autoLock(scriptListMutex);
scriptList.push_back(script);
}
//----------------------------------------------------
void QtScriptManager::removeScriptInstance(QtScriptInstance* script)
{
FCEU::autoScopedLock autoLock(scriptListMutex);
auto it = scriptList.begin();
while (it != scriptList.end())
@ -1136,6 +1206,41 @@ void QtScriptManager::frameFinishedUpdate()
FCEU_WRAPPER_UNLOCK();
}
//----------------------------------------------------
void QtScriptManager::guiUpdate()
{
FCEU_WRAPPER_LOCK();
for (auto script : scriptList)
{
script->onGuiUpdate();
}
FCEU_WRAPPER_UNLOCK();
}
//----------------------------------------------------
//---- Qt Script Monitor Thread
//----------------------------------------------------
ScriptMonitorThread_t::ScriptMonitorThread_t(QObject *parent)
: QThread(parent)
{
}
//----------------------------------------------------
void ScriptMonitorThread_t::run()
{
//printf("Script Monitor Thread is Running...\n");
QtScriptManager* manager = QtScriptManager::getInstance();
while (!isInterruptionRequested())
{
manager->scriptListMutex.lock();
for (auto script : manager->scriptList)
{
script->checkForHang();
}
manager->scriptListMutex.unlock();
msleep(100);
}
//printf("Script Monitor Thread is Stopping...\n");
}
//----------------------------------------------------
//---- Qt Script Dialog Window
//----------------------------------------------------
QScriptDialog_t::QScriptDialog_t(QWidget *parent)

View File

@ -26,8 +26,10 @@
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QJSEngine>
#include <QThread>
#include "Qt/main.h"
#include "utils/mutex.h"
#include "utils/timeStamp.h"
class QScriptDialog_t;
@ -85,13 +87,13 @@ public slots:
Q_INVOKABLE void pause();
Q_INVOKABLE void unpause();
Q_INVOKABLE bool paused();
Q_INVOKABLE int framecount();
Q_INVOKABLE int lagcount();
Q_INVOKABLE int frameCount();
Q_INVOKABLE int lagCount();
Q_INVOKABLE bool lagged();
Q_INVOKABLE void setlagflag(bool flag);
Q_INVOKABLE void setLagFlag(bool flag);
Q_INVOKABLE bool emulating();
Q_INVOKABLE void registerBefore(const QJSValue& func);
Q_INVOKABLE void registerAfter(const QJSValue& func);
Q_INVOKABLE void registerBeforeFrame(const QJSValue& func);
Q_INVOKABLE void registerAfterFrame(const QJSValue& func);
Q_INVOKABLE void registerStop(const QJSValue& func);
Q_INVOKABLE void message(const QString& msg);
Q_INVOKABLE void speedMode(const QString& mode);
@ -113,6 +115,7 @@ public:
void setDialog(QScriptDialog_t* _dialog){ dialog = _dialog; }
void reset();
QtScriptInstance* getScript(){ return script; }
QJSValue* getReadFunc(int address) { return readFunc[address]; }
QJSValue* getWriteFunc(int address) { return writeFunc[address]; }
QJSValue* getExecFunc(int address) { return execFunc[address]; }
@ -161,6 +164,36 @@ public slots:
};
} // JS
class ScriptExecutionState
{
public:
void start()
{
startTime.readNew();
executing = true;
}
void stop()
{
executing = false;
}
bool isRunning(){ return executing; }
uint64_t timeRunning()
{
FCEU::timeStampRecord now, diff;
now.readNew();
diff = now - startTime;
return diff.toMilliSeconds();
}
private:
bool executing = false;
FCEU::timeStampRecord startTime;
};
class QtScriptInstance : public QObject
{
Q_OBJECT
@ -177,6 +210,9 @@ public:
int call(const QString& funcName, const QJSValueList& args = QJSValueList());
void onFrameBegin();
void onFrameFinish();
void onGuiUpdate();
void checkForHang();
int runFunc(QJSValue &func, const QJSValueList& args = QJSValueList());
int throwError(QJSValue::ErrorType errorType, const QString &message = QString());
@ -189,6 +225,8 @@ private:
void printSymbols(QJSValue& val, int iter = 0);
void loadObjectChildren(QJSValue& jsObject, QObject* obj);
ScriptExecutionState* getExecutionState();
QJSEngine* engine = nullptr;
QScriptDialog_t* dialog = nullptr;
JS::EmuScriptObject* emu = nullptr;
@ -197,19 +235,34 @@ private:
QJSValue *onFrameBeginCallback = nullptr;
QJSValue *onFrameFinishCallback = nullptr;
QJSValue *onScriptStopCallback = nullptr;
QJSValue *onGuiUpdateCallback = nullptr;
ScriptExecutionState guiFuncState;
ScriptExecutionState emuFuncState;
bool running = false;
public slots:
Q_INVOKABLE void print(const QString& msg);
Q_INVOKABLE void loadUI(const QString& uiFilePath);
Q_INVOKABLE QString openFileBrowser(const QString& initialPath = QString());
Q_INVOKABLE void registerBefore(const QJSValue& func);
Q_INVOKABLE void registerAfter(const QJSValue& func);
Q_INVOKABLE void registerBeforeEmuFrame(const QJSValue& func);
Q_INVOKABLE void registerAfterEmuFrame(const QJSValue& func);
Q_INVOKABLE void registerStop(const QJSValue& func);
Q_INVOKABLE void registerGuiUpdate(const QJSValue& func);
Q_INVOKABLE bool onGuiThread();
Q_INVOKABLE bool onEmulationThread();
};
class ScriptMonitorThread_t : public QThread
{
Q_OBJECT
protected:
void run( void ) override;
public:
ScriptMonitorThread_t( QObject *parent = 0 );
};
class QtScriptManager : public QObject
{
Q_OBJECT
@ -228,12 +281,17 @@ public:
private:
static QtScriptManager* _instance;
FCEU::mutex scriptListMutex;
QList<QtScriptInstance*> scriptList;
FCEU::timeStampRecord lastFrameUpdate;
QTimer *periodicUpdateTimer = nullptr;
ScriptMonitorThread_t *monitorThread = nullptr;
friend class ScriptMonitorThread_t;
public slots:
void frameBeginUpdate();
void frameFinishedUpdate();
void guiUpdate();
};
class JsPropertyItem : public QTreeWidgetItem