1
0
forked from BRT/arc
arc/services/scripting/ScriptRuntime.cpp
slava cae345cf61 Implement JS-based remote scripting framework for dynamic script updates:
- Add Proof of Concept (PoC) for executing JavaScript scripts fetched remotely.
- Integrate `Black::GetProfileInfoScript` logic into a JS version with fallback to C++ on errors.
- Extend `ScreenXmlParser` with additional JS-to-C++ bridge methods.
- Update `config.ini` for scripting configuration.
- Ensure scripts are securely fetched with SHA-256 validation and use an atomic caching mechanism.
- Add developer tools for debugging JS scripts and enable easier script updates without app re-deployment.
2026-05-13 15:42:10 +07:00

136 lines
5.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ScriptRuntime.h"
#include <QDebug>
#include <QFile>
#include <QJSEngine>
#include <QJSValue>
#include <QString>
#include <QThread>
#include "ScriptBridges.h"
#include "ScriptCache.h"
#include "ScriptingConfig.h"
namespace Scripting {
namespace {
// Конверсия QJSValue → строка-результат скрипта.
// null/undefined → пустая строка (успех).
// "" → пустая строка (успех).
// строка → текст бизнес-ошибки.
// число/булево/объект → toString() как diagnostic (рассматриваем как ошибку).
QString jsValueToResultString(const QJSValue &v) {
if (v.isUndefined() || v.isNull()) return QString();
if (v.isString()) return v.toString();
return v.toString();
}
QString locationOf(const QJSValue &err) {
const int line = err.property("lineNumber").toInt();
const QString file = err.property("fileName").toString();
if (line > 0 || !file.isEmpty()) {
return QStringLiteral(" at %1:%2").arg(file.isEmpty() ? QStringLiteral("<script>") : file).arg(line);
}
return {};
}
} // namespace
ScriptRuntime::Result ScriptRuntime::run(const QString &bank,
const QString &scriptName,
const QVariantMap &params,
QString *outResult) {
QString sink;
if (!outResult) outResult = &sink;
*outResult = QString();
const QString key = bank + "/" + scriptName;
if (!ScriptingConfig::isEnabled() || !ScriptingConfig::isJsEnabledFor(key)) {
*outResult = "JS disabled for " + key;
return Result::Disabled;
}
QString sourcePath;
QString loadError;
if (!ScriptCache::resolve(bank, scriptName, &sourcePath, &loadError)) {
*outResult = "Script not available: " + loadError;
return Result::Disabled;
}
QFile f(sourcePath);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
*outResult = "Cannot read script file: " + sourcePath;
return Result::EngineError;
}
const QString source = QString::fromUtf8(f.readAll());
f.close();
QJSEngine engine;
engine.installExtensions(QJSEngine::ConsoleExtension);
// Bridges живут на стеке здесь, в worker-потоке. Эти объекты не должны
// переживать engine — QJSEngine удаляет их при разрушении (мы их newQObject'нули),
// поэтому передаём nullptr как родителя — engine берёт владение.
auto *log = new LogBridge(key);
auto *timer = new TimerBridge();
auto *adb = new AdbBridge();
auto *dao = new DaoBridge();
auto *dump = new DumpBridge();
// Bank-specific bridges. На PoC поддержан только Black.
QObject *parserBridge = nullptr;
QObject *helpersBridge = nullptr;
if (bank == QLatin1String("black")) {
auto *pb = new BlackParserBridge();
parserBridge = pb;
helpersBridge = new BlackHelpersBridge(pb);
}
QJSValue host = engine.newObject();
host.setProperty("log", engine.newQObject(log));
host.setProperty("timer", engine.newQObject(timer));
host.setProperty("adb", engine.newQObject(adb));
host.setProperty("dao", engine.newQObject(dao));
host.setProperty("dump", engine.newQObject(dump));
if (parserBridge) host.setProperty("parser", engine.newQObject(parserBridge));
if (helpersBridge) host.setProperty("helpers", engine.newQObject(helpersBridge));
// params: конвертируем QVariantMap → JS object
QJSValue jsParams = engine.toScriptValue(params);
host.setProperty("params", jsParams);
engine.globalObject().setProperty("host", host);
// Оборачиваем исходник так, чтобы он МОГ просто определять run(host) и не
// быть IIFE. Сначала evaluate определений, потом достаём `run` из globalObject.
QJSValue evalResult = engine.evaluate(source, sourcePath);
if (evalResult.isError()) {
*outResult = QStringLiteral("JS parse/eval error: %1%2")
.arg(evalResult.toString(), locationOf(evalResult));
qWarning() << "[Scripting]" << *outResult;
return Result::EngineError;
}
QJSValue runFn = engine.globalObject().property("run");
if (!runFn.isCallable()) {
*outResult = QStringLiteral("Script %1 did not define function run(host)").arg(key);
qWarning() << "[Scripting]" << *outResult;
return Result::EngineError;
}
QJSValue callResult = runFn.call(QJSValueList{} << host);
if (callResult.isError()) {
*outResult = QStringLiteral("JS runtime error: %1%2")
.arg(callResult.toString(), locationOf(callResult));
qWarning() << "[Scripting]" << *outResult;
return Result::EngineError;
}
*outResult = jsValueToResultString(callResult);
return Result::Completed;
}
} // namespace Scripting