1
0
forked from BRT/arc
arc/services/scripting/ScriptRuntime.cpp

146 lines
5.6 KiB
C++

#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,
QObject *signalSink) {
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();
auto *ocr = new OcrBridge();
auto *scriptObj = new ScriptObjectBridge(signalSink);
// Bank-specific bridges. Поддержаны: Black (полный набор), Ozon (минимум
// для get_profile_info — расширяется по мере миграции login_and_check / get_card_info).
QObject *parserBridge = nullptr;
QObject *helpersBridge = nullptr;
if (bank == QLatin1String("black")) {
auto *pb = new BlackParserBridge();
parserBridge = pb;
helpersBridge = new BlackHelpersBridge(pb);
} else if (bank == QLatin1String("ozon")) {
auto *pb = new OzonParserBridge();
parserBridge = pb;
helpersBridge = new OzonHelpersBridge(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));
host.setProperty("ocr", engine.newQObject(ocr));
host.setProperty("script", engine.newQObject(scriptObj));
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