Files
edgetx-gx12/companion/src/updates/updatecloudbuild.cpp
T

504 lines
15 KiB
C++

/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "updatecloudbuild.h"
#include "repo.h"
#include "appdata.h"
#include "eeprominterface.h"
#include "flashfirmwaredialog.h"
// these must match text strings in server response
constexpr char CLOUD_BUILD_WAITING[] {"WAITING_FOR_BUILD"};
constexpr char CLOUD_BUILD_IN_PROGRESS[] {"BUILD_IN_PROGRESS"};
constexpr char CLOUD_BUILD_SUCCESS[] {"BUILD_SUCCESS"};
UpdateCloudBuild::UpdateCloudBuild(QWidget * parent) :
UpdateInterface(parent, CID_CloudBuild, tr("CloudBuild"), Repo::REPO_TYPE_BUILD,
QString("https://cloudbuild.edgetx.org/api"), "nightly"),
m_objBody(nullptr),
m_docResp(nullptr)
{
init(); // call after UpdateInterface ctor due to virtual functions
}
UpdateCloudBuild::~UpdateCloudBuild()
{
if (m_objBody)
delete m_objBody;
if (m_docResp)
delete m_docResp;
}
QString UpdateCloudBuild::statusToString(const int status)
{
switch (status) {
case STATUS_WAITING:
return tr("waiting");
case STATUS_IN_PROGRESS:
return tr("in progress");
case STATUS_SUCCESS:
return tr("success");
case STATUS_ERROR:
return tr("error");
case STATUS_TIMEOUT:
return tr("timeout");
case STATUS_CANCELLED:
return tr("cancelled");
default:
return tr("unknown");
}
}
int UpdateCloudBuild::cloudStatusToInt(const QString status)
{
if (status == QString(CLOUD_BUILD_WAITING))
return STATUS_WAITING;
else if (status == QString(CLOUD_BUILD_IN_PROGRESS))
return STATUS_IN_PROGRESS;
else if (status == QString(CLOUD_BUILD_SUCCESS))
return STATUS_SUCCESS;
else
return STATUS_UNKNOWN;
}
void UpdateCloudBuild::assetSettingsInit()
{
if (!isSettingsIndexValid())
return;
g.component[id()].initAllAssets();
ComponentAssetData &cad = g.component[id()].asset[0];
cad.desc("binary");
cad.processes((UPDFLG_Common_Asset | UPDFLG_AsyncInstall | UPDFLG_Build) &~ UPDFLG_Decompress);
cad.flags(cad.processes() | UPDFLG_CopyFiles | UPDFLG_Locked);
cad.filterType(UpdateParameters::UFT_Exact);
cad.filter("%FWFLAVOUR%");
cad.destSubDir("FIRMWARE");
cad.copyFilterType(UpdateParameters::UFT_Pattern);
cad.copyFilter("^%FWFLAVOUR%-%LANGUAGE%.*-%RELEASE%\\.bin$");
cad.maxExpected(1);
qDebug() << "Asset settings initialised";
}
bool UpdateCloudBuild::addBuildLanguage(const QString & flag, const QJsonArray & values, QJsonArray & buildFlags)
{
QString val;
if (values.contains(params()->language.toLower()))
val = params()->language.toLower();
else if (values.contains(params()->language.toUpper()))
val = params()->language.toUpper();
else {
status()->reportProgress(tr("Radio profile language '%1' not supported").arg(params()->language), QtCriticalMsg);
return false;
}
addBuildFlag(buildFlags, flag, val);
return true;
}
bool UpdateCloudBuild::addBuildFai(const QString & flag, const QJsonArray & values, QJsonArray & buildFlags)
{
if (!m_profileOpts.contains("faichoice") && !m_profileOpts.contains("faimode"))
return true;
if (!values.contains("CHOICE") && !values.contains("YES")) {
status()->reportProgress(tr("fai_mode values do not contain CHOICE and YES"), QtCriticalMsg);
return false;
}
QString val;
if (m_profileOpts.contains("faichoice"))
val = "CHOICE";
else if (m_profileOpts.contains("faimode"))
val = "YES";
m_buildFlags.append(QString("-%1").arg("fai"));
addBuildFlag(buildFlags, flag, val);
return true;
}
void UpdateCloudBuild::addBuildFlag(QJsonArray & buildFlags, const QString & flag, const QString & val)
{
QJsonObject bldFlag;
bldFlag.insert("name", flag);
bldFlag.insert("value", val);
buildFlags.append(QJsonValue(bldFlag));
}
bool UpdateCloudBuild::arrayExists(const QJsonObject & parent, const QString child)
{
return !parent.value(child).isUndefined() && parent.value(child).isArray();
}
int UpdateCloudBuild::asyncInstall()
{
status()->reportProgress(tr("Write firmware to radio: %1").arg(g.currentProfile().burnFirmware() ? tr("true") : tr("false")), QtDebugMsg);
if (!g.currentProfile().burnFirmware())
return true;
status()->progressMessage(tr("Install"));
repo()->assets()->setFilterFlags(UPDFLG_AsyncInstall);
status()->reportProgress(tr("Asset filter applied: %1 Assets found: %2").arg(updateFlagsToString(UPDFLG_AsyncInstall)).arg(repo()->assets()->count()), QtDebugMsg);
if (repo()->assets()->count() < 1)
return true;
if (repo()->assets()->count() != params()->assets.at(0).maxExpected) {
status()->reportProgress(tr("Expected %1 asset for install but %2 found").arg(params()->assets.at(0).maxExpected).arg(repo()->assets()->count()), QtCriticalMsg);
return false;
}
QString destPath = updateDir();
if (!repo()->assets()->subDirectory().isEmpty())
destPath.append("/" % repo()->assets()->subDirectory());
const UpdateParameters::AssetParams &ap = params()->assets.at(0);
QString pattern(params()->buildFilterPattern(ap.copyFilterType, ap.copyFilter));
QRegularExpression filter(pattern, QRegularExpression::CaseInsensitiveOption);
QDirIterator it(destPath);
bool found = false;
while (it.hasNext())
{
QFileInfo file(it.next());
if ((!filter.pattern().isEmpty()) && (!file.fileName().contains(filter)))
continue;
destPath.append("/" % file.fileName());
found = true;
break;
}
if (!found) {
status()->reportProgress(tr("Firmware not found in %1 using filter %2").arg(destPath).arg(filter.pattern()), QtCriticalMsg);
return false;
}
g.currentProfile().fwName(destPath);
int ret = QMessageBox::question(status()->progress(), CPN_STR_APP_NAME, tr("Write the updated firmware to the radio now ?"), QMessageBox::Yes | QMessageBox::No);
if (ret == QMessageBox::Yes) {
FlashFirmwareDialog *dlg = new FlashFirmwareDialog(this);
dlg->exec();
dlg->deleteLater();
}
return true;
}
bool UpdateCloudBuild::buildFlaggedAsset(const int row)
{
m_jobStatus = STATUS_UNKNOWN;
m_buildFlags.clear();
m_profileOpts.clear();
m_radio = repo()->assets()->name(row);
status()->reportProgress(tr("Building firmware for: %1").arg(m_radio));
status()->progressMessage(tr("Building firmware for: %1").arg(m_radio));
if (params()->logLevel == QtDebugMsg) {
m_logDir = QString("%1/logs").arg(downloadDir());
if (!QDir().mkpath(m_logDir)) {
status()->reportProgress(tr("Failed to create directory %1!").arg(m_logDir), QtCriticalMsg);
return false;
}
}
Firmware *fw = getCurrentFirmware();
m_profileOpts = fw->getId().split("-");
// [0] = edgetx
// [1] = radio id
// [last] = language code
QJsonObject target;
if (params()->logLevel == QtDebugMsg)
network()->saveJsonObjToFile(*repo()->config(), QString("%1/targets.txt").arg(m_logDir));
if (!objectExists(*repo()->config(), "targets")) {
status()->reportProgress(tr("Unexpected format for build targets meta data"), QtCriticalMsg);
return false;
}
const QJsonObject &targets = repo()->config()->value("targets").toObject();
target = targets.value(m_radio).toObject();
if (target.isEmpty()) {
status()->reportProgress(tr("No build support for target %1").arg(m_radio), QtCriticalMsg);
return false;
}
if (params()->logLevel == QtDebugMsg)
network()->saveJsonObjToFile(target, QString("%1/%2_target.txt").arg(m_logDir).arg(m_radio));
QJsonArray targetTags = target.value("tags").toArray();
if (targetTags.isEmpty()) {
status()->reportProgress(tr("Build target %1 has no valid tags").arg(m_radio), QtWarningMsg);
}
const QJsonObject flags = repo()->config()->value("flags").toObject();
if (flags.isEmpty()) {
status()->reportProgress(tr("No flag entries found"), QtCriticalMsg);
return false;
}
QJsonArray arrBuildFlags;
QStringList flagKeys = flags.keys();
for (auto i = flagKeys.cbegin(), end = flagKeys.cend(); i != end; ++i) {
const QJsonObject &flag = flags.value(*i).toObject();
const QJsonArray &fvals = flag.value("values").toArray();
if (*i == "language") {
if (!addBuildLanguage(*i, fvals, arrBuildFlags))
return false;
}
else if (*i == "fai_mode") {
if (!addBuildFai(*i, fvals, arrBuildFlags))
return false;
}
//==================================================================================
// TODO flags more to be added including custom hardware target build eg SPACEMOUSE
//==================================================================================
}
if (m_objBody)
delete m_objBody;
m_objBody = new QJsonObject();
m_objBody->insert("release", QJsonValue(params()->releaseUpdate));
m_objBody->insert("target", QJsonValue(m_radio));
m_objBody->insert("flags", QJsonValue(arrBuildFlags));
QJsonDocument *docBody = new QJsonDocument(*m_objBody);
if (m_docResp)
delete m_docResp;
m_docResp = new QJsonDocument();
m_buildStartTime = QTime::currentTime();
if (params()->logLevel == QtDebugMsg)
network()->saveJsonDocToFile(docBody, QString("%1/%2_body.txt").arg(m_logDir).arg(m_radio));
network()->submitRequest(tr("Submit build request"), repo()->urlJobs(), docBody, m_docResp);
if (params()->logLevel == QtDebugMsg)
network()->saveBufferToFile(QString("%1/%2_buffer.txt").arg(m_logDir).arg(m_radio));
if (!network()->isSuccess()) {
status()->reportProgress(tr("Failed to initiate build job"), QtCriticalMsg);
return false;
}
if (!m_docResp->isObject()) {
status()->reportProgress(tr("Unexpected response format when submitting build job"), QtCriticalMsg);
return false;
}
if (params()->logLevel == QtDebugMsg)
network()->saveJsonDocToFile(m_docResp, QString("%1/%2_response.txt").arg(m_logDir).arg(m_radio));
const QJsonObject &obj = m_docResp->object();
if (stringExists(obj, "error")) {
status()->reportProgress(tr("Build error: %1").arg(obj.value("error").toString()), QtCriticalMsg);
return false;
}
else if (obj.value("target").toString() != m_radio || obj.value("release").toString() != params()->releaseUpdate) {
status()->reportProgress(tr("Process status not returned when submitting build job"), QtCriticalMsg);
return false;
}
else {
m_jobStatus = cloudStatusToInt(obj.value("status").toString());
}
if (isStatusInProgress())
waitForBuildFinish();
status()->reportProgress(tr("Build finish status: %1").arg(statusToString(m_jobStatus)));
// ensure progress bar not in wait state
status()->setMaximum(100);
if (m_jobStatus == STATUS_SUCCESS && setAssetDownload())
return true;
return false;
}
void UpdateCloudBuild::cancel()
{
// this does not cancel the build job on the server but only interrupts the client side
status()->reportProgress(tr("Build cancelled"), QtWarningMsg);
m_jobStatus = STATUS_CANCELLED;
cleanup();
}
void UpdateCloudBuild::checkStatus()
{
if (m_buildStartTime.secsTo(QTime::currentTime()) > 180) {
status()->reportProgress(tr("Build timeout. Retry later."), QtWarningMsg);
m_jobStatus = STATUS_TIMEOUT;
cleanup();
return;
}
getStatus();
if (!isStatusInProgress())
cleanup();
}
void UpdateCloudBuild::cleanup()
{
if (m_timer.isActive())
m_timer.stop();
if (m_eventLoop.isRunning())
m_eventLoop.quit();
}
bool UpdateCloudBuild::getStatus()
{
m_jobStatus = STATUS_UNKNOWN;
QJsonDocument *docBody = new QJsonDocument(*m_objBody);
if (m_docResp)
delete m_docResp;
m_docResp = new QJsonDocument();
network()->submitRequest(tr("Submit get build status"), repo()->urlStatus(), docBody, m_docResp);
if (params()->logLevel == QtDebugMsg)
network()->saveJsonDocToFile(m_docResp, QString("%1/%2_status_%3.txt").arg(m_logDir).arg(m_radio).arg(QTime::currentTime().toString("hhmmss")));
if (m_docResp->isObject())
m_jobStatus = cloudStatusToInt(m_docResp->object().value("status").toString());
if (m_jobStatus == STATUS_UNKNOWN) {
status()->reportProgress(tr("Build status unknown"), QtCriticalMsg);
return false;
}
return true;
}
bool UpdateCloudBuild::isStatusInProgress()
{
if (m_jobStatus == STATUS_WAITING || m_jobStatus == STATUS_IN_PROGRESS)
return true;
return false;
}
bool UpdateCloudBuild::objectExists(const QJsonObject & parent, const QString child)
{
return !parent.value(child).isUndefined() && parent.value(child).isObject();
}
bool UpdateCloudBuild::setAssetDownload()
{
// this format MUST align with the asset copy filter
QString name = QString("%1-%2%3-%4.bin").arg(m_radio).arg(params()->language.toLower()).arg(m_buildFlags/* has a leading hyphen*/).arg(repo()->releases()->name());
repo()->assets()->setDownloadName(name.toLower());
const QJsonObject &obj = m_docResp->object();
if (arrayExists(obj, "artifacts")) {
const QJsonArray &artifacts = obj.value("artifacts").toArray();
if (artifacts.count() > 1) {
status()->reportProgress(tr("Build status contains %1 artifacts when only 1 expected").arg(artifacts.count()), QtWarningMsg);
}
for (int i = 0; i < artifacts.count(); i++) {
if (artifacts[i].isObject()) {
const QJsonObject &artifact = artifacts[i].toObject();
if (stringExists(artifact, "slug") && artifact.value("slug").toString() == "firmware") {
if (stringExists(artifact, "download_url")) {
repo()->assets()->setDownloadUrl(artifact.value("download_url").toString());
}
else {
status()->reportProgress(tr("Build status does not contain download url"), QtCriticalMsg);
return false;
}
}
else {
status()->reportProgress(tr("Build status does not contain firmware artifact"), QtCriticalMsg);
return false;
}
}
else {
status()->reportProgress(tr("Build status firmware artifact not in expected format"), QtCriticalMsg);
return false;
}
}
}
else {
status()->reportProgress(tr("Build status does not contain artifacts"), QtCriticalMsg);
return false;
}
return true;
}
bool UpdateCloudBuild::stringExists(const QJsonObject & parent, const QString child)
{
return !parent.value(child).isUndefined() && parent.value(child).isString();
}
void UpdateCloudBuild::waitForBuildFinish()
{
status()->progressMessage(tr("Waiting for build to finish..."));
status()->setMaximum(0); // sets progress bar to wait state
m_timer.setInterval(2000); // check status every 2 seconds
connect(this, &UpdateInterface::stopping, this, &UpdateCloudBuild::cancel);
connect(&m_timer, &QTimer::timeout, this, &UpdateCloudBuild::checkStatus);
m_timer.start();
m_eventLoop.exec();
disconnect(&m_timer, &QTimer::timeout, this, &UpdateCloudBuild::checkStatus);
disconnect(this, &UpdateInterface::stopping, this, &UpdateCloudBuild::cancel);
}