- 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.
160 lines
5.5 KiB
C++
160 lines
5.5 KiB
C++
#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
|