#include "ScriptUpdater.h" #include #include #include #include #include #include #include #include #include #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(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(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