1
0
forked from BRT/arc
arc/services/scripting/ScriptUpdater.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

160 lines
5.5 KiB
C++
Raw Permalink 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 "ScriptUpdater.h"
#include <QCryptographicHash>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>
#include "ScriptCache.h"
#include "ScriptingConfig.h"
namespace Scripting {
namespace {
constexpr char kReplyKindManifest[] = "manifest";
constexpr char kReplyKindScript[] = "script";
}
ScriptUpdater::ScriptUpdater(QObject *parent)
: QObject(parent), m_net(new QNetworkAccessManager(this)) {}
ScriptUpdater::~ScriptUpdater() = default;
void ScriptUpdater::start() {
if (!ScriptingConfig::isEnabled()) {
qDebug() << "[ScriptUpdater] disabled in config";
emit finished();
return;
}
const QString url = ScriptingConfig::manifestUrl();
if (url.isEmpty()) {
qDebug() << "[ScriptUpdater] manifest_url empty — пропускаю обновление, использую bundled/cache";
emit finished();
return;
}
QNetworkRequest req((QUrl(url)));
req.setRawHeader("Accept", "application/json");
QNetworkReply *reply = m_net->get(req);
reply->setProperty("kind", kReplyKindManifest);
connect(reply, &QNetworkReply::finished, this, &ScriptUpdater::onManifestReply);
}
void ScriptUpdater::onManifestReply() {
auto *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) { scheduleFinish(); return; }
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[ScriptUpdater] manifest fetch failed:" << reply->errorString();
scheduleFinish();
return;
}
const QByteArray body = reply->readAll();
QJsonParseError err{};
const QJsonDocument doc = QJsonDocument::fromJson(body, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "[ScriptUpdater] manifest parse error:" << err.errorString();
scheduleFinish();
return;
}
const QJsonArray arr = doc.object().value("scripts").toArray();
for (const QJsonValue &v : arr) {
const QJsonObject o = v.toObject();
ScriptItem item;
item.bank = o.value("bank").toString();
item.script = o.value("script").toString();
item.version = o.value("version").toString();
item.url = o.value("url").toString();
item.sha256 = o.value("sha256").toString().toLower();
if (item.bank.isEmpty() || item.script.isEmpty() || item.url.isEmpty() || item.sha256.isEmpty()) {
qWarning() << "[ScriptUpdater] skipping invalid manifest entry:" << o;
continue;
}
const QString installed = ScriptCache::installedVersion(item.bank, item.script);
if (!installed.isEmpty() && installed == item.version) {
qDebug() << "[ScriptUpdater]" << item.bank << "/" << item.script
<< "version" << item.version << "already installed";
continue;
}
m_queue.append(item);
}
if (m_queue.isEmpty()) {
qDebug() << "[ScriptUpdater] nothing to update";
scheduleFinish();
return;
}
qInfo() << "[ScriptUpdater] queued" << m_queue.size() << "script update(s)";
downloadNext();
}
void ScriptUpdater::downloadNext() {
if (m_queue.isEmpty()) {
if (m_inFlight == 0) scheduleFinish();
return;
}
const ScriptItem item = m_queue.takeFirst();
QNetworkRequest req{QUrl(item.url)};
QNetworkReply *reply = m_net->get(req);
reply->setProperty("kind", kReplyKindScript);
reply->setProperty("bank", item.bank);
reply->setProperty("script", item.script);
reply->setProperty("version", item.version);
reply->setProperty("sha256", item.sha256);
m_inFlight++;
connect(reply, &QNetworkReply::finished, this, &ScriptUpdater::onScriptReply);
}
void ScriptUpdater::onScriptReply() {
auto *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply) { scheduleFinish(); return; }
reply->deleteLater();
m_inFlight--;
const QString bank = reply->property("bank").toString();
const QString script = reply->property("script").toString();
const QString version = reply->property("version").toString();
const QString expectedSha = reply->property("sha256").toString().toLower();
const QString tag = bank + "/" + script;
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[ScriptUpdater] download failed for" << tag << ":" << reply->errorString();
downloadNext();
return;
}
const QByteArray body = reply->readAll();
const QString actualSha = QString::fromLatin1(
QCryptographicHash::hash(body, QCryptographicHash::Sha256).toHex());
if (actualSha != expectedSha) {
qWarning() << "[ScriptUpdater] SHA-256 mismatch for" << tag
<< "expected" << expectedSha << "actual" << actualSha;
downloadNext();
return;
}
QString installErr;
if (!ScriptCache::installFromBytes(bank, script, version, body, &installErr)) {
qWarning() << "[ScriptUpdater] install failed for" << tag << ":" << installErr;
downloadNext();
return;
}
qInfo() << "[ScriptUpdater] installed" << tag << "version" << version;
downloadNext();
}
void ScriptUpdater::scheduleFinish() {
// Гарантирует, что finished() уйдёт после возврата управления event loop'у.
QMetaObject::invokeMethod(this, [this]() { emit finished(); }, Qt::QueuedConnection);
}
} // namespace Scripting