File indexing completed on 2024-12-29 04:49:58
0001 /* 0002 SPDX-FileCopyrightText: 2017-2021 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "extractorscriptengine_p.h" 0008 #include "extractordocumentnode.h" 0009 #include "extractordocumentprocessor.h" 0010 #include "extractorengine.h" 0011 #include "extractorresult.h" 0012 #include "scriptextractor.h" 0013 #include "logging.h" 0014 0015 #include "jsapi/barcode.h" 0016 #include "jsapi/bytearray.h" 0017 #include "jsapi/extractorengine.h" 0018 #include "jsapi/jsonld.h" 0019 0020 #include <QFile> 0021 #include <QJSEngine> 0022 #include <QJSValueIterator> 0023 #include <QScopeGuard> 0024 #include <QThread> 0025 #include <QTimer> 0026 0027 using namespace KItinerary; 0028 0029 namespace KItinerary { 0030 class ExtractorScriptEnginePrivate { 0031 public: 0032 ~ExtractorScriptEnginePrivate(); 0033 bool loadScript(const QString &fileName); 0034 0035 JsApi::Barcode *m_barcodeApi = nullptr; 0036 JsApi::JsonLd *m_jsonLdApi = nullptr; 0037 JsApi::ExtractorEngine *m_engineApi = nullptr; 0038 QJSEngine m_engine; 0039 0040 QThread m_watchdogThread; 0041 QTimer *m_watchdogTimer = nullptr; 0042 }; 0043 } 0044 0045 ExtractorScriptEnginePrivate::~ExtractorScriptEnginePrivate() 0046 { 0047 m_watchdogTimer->deleteLater(); 0048 m_watchdogThread.quit(); 0049 m_watchdogThread.wait(); 0050 } 0051 0052 ExtractorScriptEngine::ExtractorScriptEngine() = default; 0053 ExtractorScriptEngine::~ExtractorScriptEngine() = default; 0054 0055 void ExtractorScriptEngine::ensureInitialized() 0056 { 0057 if (d) { 0058 return; 0059 } 0060 0061 d = std::make_unique<ExtractorScriptEnginePrivate>(); 0062 d->m_engine.installExtensions(QJSEngine::ConsoleExtension); 0063 d->m_jsonLdApi = new JsApi::JsonLd(&d->m_engine); 0064 d->m_engine.globalObject().setProperty(QStringLiteral("JsonLd"), d->m_engine.newQObject(d->m_jsonLdApi)); 0065 d->m_barcodeApi = new JsApi::Barcode; 0066 d->m_engine.globalObject().setProperty(QStringLiteral("Barcode"), d->m_engine.newQObject(d->m_barcodeApi)); 0067 d->m_engine.globalObject().setProperty(QStringLiteral("ByteArray"), d->m_engine.newQObject(new JsApi::ByteArray)); 0068 d->m_engineApi = new JsApi::ExtractorEngine(&d->m_engine); 0069 d->m_engine.globalObject().setProperty(QStringLiteral("ExtractorEngine"), d->m_engine.newQObject(d->m_engineApi)); 0070 0071 d->m_watchdogThread.start(); 0072 d->m_watchdogTimer = new QTimer; 0073 d->m_watchdogTimer->setInterval(std::chrono::seconds(1)); 0074 d->m_watchdogTimer->setSingleShot(true); 0075 d->m_watchdogTimer->moveToThread(&d->m_watchdogThread); 0076 QObject::connect(d->m_watchdogTimer, &QTimer::timeout, &d->m_engine, [this]() { d->m_engine.setInterrupted(true); }, Qt::DirectConnection); 0077 } 0078 0079 void ExtractorScriptEngine::setExtractorEngine(ExtractorEngine *engine) 0080 { 0081 ensureInitialized(); 0082 d->m_engineApi->setEngine(engine); 0083 d->m_barcodeApi->setDecoder(engine->barcodeDecoder()); 0084 } 0085 0086 // produce the same output as the JS engine error result fileName property would have 0087 static QString fileNameToUrl(const QString &fileName) 0088 { 0089 if (fileName.startsWith(QLatin1Char(':'))) { 0090 return QLatin1StringView("qrc:/") + QStringView(fileName).mid(1); 0091 } 0092 return QUrl::fromLocalFile(fileName).toString(); 0093 } 0094 0095 static void printScriptError(const QJSValue &result, const QString &fileNameFallback) 0096 { 0097 const auto fileName = result.property(QStringLiteral("fileName")); 0098 // don't change the formatting without adjusting KItinerary Workbench too! 0099 qCWarning(Log).noquote().nospace() << "JS ERROR: [" << (fileName.isString() ? fileName.toString() : fileNameToUrl(fileNameFallback)) 0100 << "]:" << result.property(QStringLiteral("lineNumber")).toInt() << ": " << result.toString(); 0101 } 0102 0103 bool ExtractorScriptEnginePrivate::loadScript(const QString &fileName) 0104 { 0105 // TODO we could skip this is if the right script is already loaded 0106 // we cannot do this unconditionally however without breaking KItinerary Workbench's live editing 0107 if (fileName.isEmpty()) { 0108 return false; 0109 } 0110 0111 QFile f(fileName); 0112 if (!f.open(QFile::ReadOnly)) { 0113 qCWarning(Log) << "Failed to open extractor script" << f.fileName() << f.errorString(); 0114 return false; 0115 } 0116 0117 auto result = m_engine.evaluate(QString::fromUtf8(f.readAll()), f.fileName()); 0118 if (result.isError()) { 0119 printScriptError(result, fileName); 0120 return false; 0121 } 0122 0123 return true; 0124 } 0125 0126 ExtractorResult ExtractorScriptEngine::execute(const ScriptExtractor *extractor, const ExtractorDocumentNode &node, const ExtractorDocumentNode &triggerNode) const 0127 { 0128 const_cast<ExtractorScriptEngine*>(this)->ensureInitialized(); 0129 0130 // watchdog setup 0131 QMetaObject::invokeMethod(d->m_watchdogTimer, qOverload<>(&QTimer::start)); 0132 const auto scopeCleanup = qScopeGuard([this]() { 0133 QMetaObject::invokeMethod(d->m_watchdogTimer, qOverload<>(&QTimer::stop)); 0134 }); 0135 d->m_engine.setInterrupted(false); 0136 0137 if (!d->loadScript(extractor->scriptFileName())) { 0138 return {}; 0139 } 0140 0141 auto mainFunc = d->m_engine.globalObject().property(extractor->scriptFunction()); 0142 if (!mainFunc.isCallable()) { 0143 qCWarning(Log) << "Script entry point not found!" << extractor->scriptFunction(); 0144 return {}; 0145 } 0146 0147 qCDebug(Log) << "Running script extractor" << extractor->scriptFileName() << extractor->scriptFunction(); 0148 node.setScriptEngine(&d->m_engine); 0149 triggerNode.setScriptEngine(&d->m_engine); 0150 const auto engineReset = qScopeGuard([&node, &triggerNode, this]{ 0151 d->m_engineApi->clear(); 0152 node.setScriptEngine(nullptr); 0153 triggerNode.setScriptEngine(nullptr); 0154 }); 0155 0156 d->m_jsonLdApi->setContextDate(node.contextDateTime()); 0157 d->m_engineApi->setCurrentNode(node); 0158 0159 const auto nodeArg = d->m_engine.toScriptValue(node); 0160 const auto dataArg = nodeArg.property(QLatin1StringView("content")); 0161 const auto triggerArg = d->m_engine.toScriptValue(triggerNode); 0162 QJSValueList args{ dataArg, nodeArg, triggerArg }; 0163 0164 const auto result = mainFunc.call(args); 0165 if (result.isError()) { 0166 printScriptError(result, extractor->scriptFileName()); 0167 return {}; 0168 } 0169 0170 QJsonArray out; 0171 if (result.isArray()) { 0172 QJSValueIterator it(result); 0173 while (it.hasNext()) { 0174 it.next(); 0175 if (it.value().isObject()) { 0176 out.push_back(QJsonValue::fromVariant(it.value().toVariant())); 0177 } 0178 } 0179 } else if (result.isObject()) { 0180 out.push_back(QJsonValue::fromVariant(result.toVariant())); 0181 } else { 0182 qCWarning(Log) << "Invalid result type from script"; 0183 } 0184 0185 return out; 0186 }