File indexing completed on 2024-07-21 06:28:25

0001 /*
0002     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "indiwebmanager.h"
0008 
0009 #include "auxiliary/ksnotification.h"
0010 #include "driverinfo.h"
0011 #include "drivermanager.h"
0012 #include "Options.h"
0013 #include "profileinfo.h"
0014 
0015 #include <QJsonArray>
0016 #include <QJsonDocument>
0017 #include <QJsonObject>
0018 #include <QNetworkReply>
0019 #include <QtConcurrent>
0020 
0021 #include "ekos_debug.h"
0022 
0023 namespace INDI
0024 {
0025 
0026 namespace WebManager
0027 {
0028 bool getWebManagerResponse(QNetworkAccessManager::Operation operation, const QUrl &url, QJsonDocument *reply,
0029                            QByteArray *data)
0030 {
0031     QNetworkAccessManager manager;
0032     QNetworkReply *response = nullptr;
0033     QNetworkRequest request;
0034 
0035     request.setUrl(url);
0036 
0037     if (data)
0038     {
0039         request.setRawHeader("Content-Type", "application/json");
0040         request.setRawHeader("Content-Length", QByteArray::number(data->size()));
0041     }
0042 
0043     switch (operation)
0044     {
0045         case QNetworkAccessManager::GetOperation:
0046             response = manager.get(request);
0047             break;
0048 
0049         case QNetworkAccessManager::PostOperation:
0050             if (data)
0051                 response = manager.post(request, *data);
0052             else
0053                 response = manager.post(request, QByteArray());
0054             break;
0055 
0056         case QNetworkAccessManager::DeleteOperation:
0057             response = manager.deleteResource(request);
0058             break;
0059 
0060         case QNetworkAccessManager::PutOperation:
0061             response = manager.put(request, *data);
0062             break;
0063 
0064         default:
0065             return false;
0066     }
0067 
0068     // Wait synchronously
0069     QEventLoop event;
0070     QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
0071     event.exec();
0072 
0073     if (response->error() == QNetworkReply::NoError)
0074     {
0075         if (reply)
0076         {
0077             QJsonParseError parseError;
0078             *reply = QJsonDocument::fromJson(response->readAll(), &parseError);
0079 
0080             if (parseError.error != QJsonParseError::NoError)
0081             {
0082                 qDebug() << Q_FUNC_INFO << "INDI: JSon error during parsing " << parseError.errorString();
0083                 return false;
0084             }
0085         }
0086 
0087         return true;
0088     }
0089     else
0090     {
0091         qDebug() << Q_FUNC_INFO << "INDI: Error communicating with INDI Web Manager: " << response->errorString();
0092         return false;
0093     }
0094 }
0095 
0096 bool isOnline(const QSharedPointer<ProfileInfo> &pi)
0097 {
0098     QTimer timer;
0099     timer.setSingleShot(true);
0100     QNetworkAccessManager manager;
0101     QUrl url(QString("http://%1:%2/api/server/status").arg(pi->host).arg(pi->INDIWebManagerPort));
0102     QNetworkReply *response = manager.get(QNetworkRequest(url));
0103 
0104     // Wait synchronously
0105     QEventLoop event;
0106     QObject::connect(&timer, &QTimer::timeout, &event, &QEventLoop::quit);
0107     QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
0108     timer.start(3000);
0109     event.exec();
0110 
0111     if (timer.isActive() && response->error() == QNetworkReply::NoError)
0112         return true;
0113     // Fallback to default if DNS lookup fails for .local
0114     else if (pi->host.contains(".local"))
0115     {
0116         QUrl url(QString("http://10.250.250.1:8624/api/server/status"));
0117         QNetworkReply *response = manager.get(QNetworkRequest(url));
0118         // Wait synchronously
0119         QEventLoop event;
0120         QObject::connect(&timer, &QTimer::timeout, &event, &QEventLoop::quit);
0121         QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
0122         timer.start(3000);
0123         event.exec();
0124 
0125         if (timer.isActive() && response->error() == QNetworkReply::NoError)
0126         {
0127             pi->host = "10.250.250.1";
0128             return true;
0129         }
0130     }
0131 
0132     return false;
0133 }
0134 
0135 bool checkVersion(const QSharedPointer<ProfileInfo> &pi)
0136 {
0137     QNetworkAccessManager manager;
0138     QUrl url(QString("http://%1:%2/api/info/version").arg(pi->host).arg(pi->INDIWebManagerPort));
0139 
0140     QJsonDocument json;
0141     if (getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
0142     {
0143         QJsonObject version = json.object();
0144         if (version.contains("version") == false)
0145             return false;
0146         qInfo(KSTARS_EKOS) << "Detected Web Manager version" << version["version"].toString();
0147         return true;
0148     }
0149     return false;
0150 }
0151 
0152 bool syncCustomDrivers(const QSharedPointer<ProfileInfo> &pi)
0153 {
0154     QNetworkAccessManager manager;
0155     QUrl url(QString("http://%1:%2/api/profiles/custom").arg(pi->host).arg(pi->INDIWebManagerPort));
0156 
0157     QStringList customDriversLabels;
0158     QMapIterator<QString, QString> i(pi->drivers);
0159     while (i.hasNext())
0160     {
0161         QString name       = i.next().value();
0162         auto driver = DriverManager::Instance()->findDriverByName(name);
0163 
0164         if (driver.isNull())
0165             driver = DriverManager::Instance()->findDriverByLabel(name);
0166         if (driver && driver->getDriverSource() == CUSTOM_SOURCE)
0167             customDriversLabels << driver->getLabel();
0168     }
0169 
0170     // Search for locked filter by filter color name
0171     const QList<QVariantMap> &customDrivers = DriverManager::Instance()->getCustomDrivers();
0172 
0173     for (auto &label : customDriversLabels)
0174     {
0175         auto pos = std::find_if(customDrivers.begin(), customDrivers.end(), [label](QVariantMap oneDriver)
0176         {
0177             return (oneDriver["Label"] == label);
0178         });
0179 
0180         if (pos == customDrivers.end())
0181             continue;
0182 
0183         QVariantMap driver = (*pos);
0184         QJsonObject jsonDriver = QJsonObject::fromVariantMap(driver);
0185 
0186         QByteArray data    = QJsonDocument(jsonDriver).toJson();
0187         getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
0188     }
0189 
0190     return true;
0191 }
0192 
0193 bool areDriversRunning(const QSharedPointer<ProfileInfo> &pi)
0194 {
0195     QUrl url(QString("http://%1:%2/api/server/drivers").arg(pi->host).arg(pi->INDIWebManagerPort));
0196     QJsonDocument json;
0197 
0198     if (getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
0199     {
0200         QJsonArray array = json.array();
0201 
0202         if (array.isEmpty())
0203             return false;
0204 
0205         QStringList piExecDrivers;
0206         QMapIterator<QString, QString> i(pi->drivers);
0207         while (i.hasNext())
0208         {
0209             QString name       = i.next().value();
0210             auto driver = DriverManager::Instance()->findDriverByName(name);
0211 
0212             if (driver.isNull())
0213                 driver = DriverManager::Instance()->findDriverByLabel(name);
0214             if (driver)
0215                 piExecDrivers << driver->getExecutable();
0216         }
0217 
0218         if (array.count() < piExecDrivers.count())
0219             return false;
0220 
0221         // Get all the drivers running remotely
0222         QStringList webManagerDrivers;
0223         for (auto value : array)
0224         {
0225             QJsonObject driver = value.toObject();
0226             // Old Web Manager API API
0227             QString exec = driver["driver"].toString();
0228             if (exec.isEmpty())
0229                 // New v0.1.5+ Web Manager API
0230                 exec = driver["binary"].toString();
0231             webManagerDrivers << exec;
0232         }
0233 
0234         // Make sure all the profile drivers are running there
0235         for (auto &oneDriverExec : piExecDrivers)
0236         {
0237             if (webManagerDrivers.contains(oneDriverExec) == false)
0238             {
0239                 KSNotification::error(i18n("Driver %1 failed to start on the remote INDI server.", oneDriverExec));
0240                 qCritical(KSTARS_EKOS) << "Driver" << oneDriverExec << "failed to start on the remote INDI server!";
0241                 return false;
0242             }
0243         }
0244 
0245         return true;
0246     }
0247 
0248     return false;
0249 }
0250 
0251 bool syncProfile(const QSharedPointer<ProfileInfo> &pi)
0252 {
0253     QUrl url;
0254     QJsonDocument jsonDoc;
0255     QJsonParseError jsonError;
0256     QByteArray data;
0257     QJsonArray profileScripts;
0258 
0259     //Add Profile
0260     url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
0261     getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
0262 
0263     // If we have scripts, let's try to parse and send them.
0264     if (pi->scripts.isEmpty() == false)
0265     {
0266         auto doc = QJsonDocument::fromJson(pi->scripts, &jsonError);
0267 
0268         if (jsonError.error == QJsonParseError::NoError)
0269         {
0270             profileScripts = doc.array();
0271             // Update profile info
0272             url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
0273             QJsonObject profileObject{ { "port", pi->port }, {"scripts", profileScripts} };
0274             jsonDoc = QJsonDocument(profileObject);
0275             data    = jsonDoc.toJson();
0276             getWebManagerResponse(QNetworkAccessManager::PutOperation, url, nullptr, &data);
0277         }
0278     }
0279 
0280     // Add drivers
0281     url = QUrl(QString("http://%1:%2/api/profiles/%3/drivers").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
0282     QJsonArray driverArray;
0283     QMapIterator<QString, QString> i(pi->drivers);
0284 
0285     // In case both Guider + CCD are Multiple-Devices-Per-Driver type
0286     // Then we should not define guider as a separate driver since that would start the driver executable twice
0287     // when we only need it once
0288     if (pi->drivers.contains("Guider"))
0289     {
0290         if (pi->drivers["Guider"] == pi->drivers["CCD"])
0291         {
0292             QSharedPointer<DriverInfo> guiderInfo;
0293             if ((guiderInfo = DriverManager::Instance()->findDriverByName(pi->drivers["Guider"])).isNull())
0294             {
0295                 if ((guiderInfo = DriverManager::Instance()->findDriverByLabel(pi->drivers["Guider"])).isNull())
0296                 {
0297                     guiderInfo = DriverManager::Instance()->findDriverByExec(pi->drivers["Guider"]);
0298                 }
0299             }
0300 
0301             if (guiderInfo && guiderInfo->getAuxInfo().value("mdpd", false).toBool())
0302             {
0303                 pi->drivers.remove("Guider");
0304                 i = QMapIterator<QString, QString>(pi->drivers);
0305             }
0306         }
0307     }
0308 
0309     // Regular Drivers
0310     while (i.hasNext())
0311         driverArray.append(QJsonObject({{"label", i.next().value()}}));
0312 
0313     // Remote Drivers
0314     if (pi->remotedrivers.isEmpty() == false)
0315     {
0316         for (auto &remoteDriver : pi->remotedrivers.split(","))
0317         {
0318             driverArray.append(QJsonObject({{"remote", remoteDriver}}));
0319         }
0320     }
0321 
0322     QJsonArray sortedList;
0323     for (const auto &oneRule : qAsConst(profileScripts))
0324     {
0325         auto matchingDriver = std::find_if(driverArray.begin(), driverArray.end(), [oneRule](const auto & oneDriver)
0326         {
0327             return oneDriver.toObject()["label"].toString() == oneRule.toObject()["Driver"].toString();
0328         });
0329 
0330         if (matchingDriver != driverArray.end())
0331         {
0332             sortedList.append(*matchingDriver);
0333         }
0334     }
0335 
0336     // If we have any profile scripts drivers, let's re-sort managed drivers
0337     // so that profile script drivers
0338     if (!sortedList.isEmpty())
0339     {
0340         for (const auto oneDriver : driverArray)
0341         {
0342             if (sortedList.contains(oneDriver) == false)
0343                 sortedList.append(oneDriver);
0344         }
0345 
0346         driverArray = sortedList;
0347     }
0348 
0349     data    = QJsonDocument(driverArray).toJson();
0350     getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
0351 
0352     return true;
0353 }
0354 
0355 bool startProfile(const QSharedPointer<ProfileInfo> &pi)
0356 {
0357     // First make sure profile is created and synced on web manager
0358     syncProfile(pi);
0359 
0360     // Start profile
0361     QUrl url(QString("http://%1:%2/api/server/start/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
0362     getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
0363 
0364     // Make sure drivers are running
0365     // Try up to 3 times
0366     for (int i = 0; i < 3; i++)
0367     {
0368         if (areDriversRunning(pi))
0369             return true;
0370     }
0371 
0372     return false;
0373 }
0374 
0375 bool stopProfile(const QSharedPointer<ProfileInfo> &pi)
0376 {
0377     // Stop profile
0378     QUrl url(QString("http://%1:%2/api/server/stop").arg(pi->host).arg(pi->INDIWebManagerPort));
0379     return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
0380 }
0381 
0382 bool restartDriver(const QSharedPointer<ProfileInfo> &pi, const QString &label)
0383 {
0384     QUrl url(QString("http://%1:%2/api/drivers/restart/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(label));
0385     return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
0386 }
0387 }
0388 
0389 // Async version of the Web Manager
0390 namespace AsyncWebManager
0391 {
0392 
0393 QFuture<bool> isOnline(const QSharedPointer<ProfileInfo> &pi)
0394 {
0395     return QtConcurrent::run(WebManager::isOnline, pi);
0396 }
0397 
0398 QFuture<bool> isStellarMate(const QSharedPointer<ProfileInfo> &pi)
0399 {
0400     return QtConcurrent::run(WebManager::checkVersion, pi);
0401 }
0402 
0403 QFuture<bool> syncCustomDrivers(const QSharedPointer<ProfileInfo> &pi)
0404 {
0405     return QtConcurrent::run(WebManager::syncCustomDrivers, pi);
0406 }
0407 
0408 QFuture<bool> areDriversRunning(const QSharedPointer<ProfileInfo> &pi)
0409 {
0410     return QtConcurrent::run(WebManager::areDriversRunning, pi);
0411 }
0412 
0413 QFuture<bool> syncProfile(const QSharedPointer<ProfileInfo> &pi)
0414 {
0415     return QtConcurrent::run(WebManager::syncProfile, pi);
0416 }
0417 
0418 QFuture<bool> startProfile(const QSharedPointer<ProfileInfo> &pi)
0419 {
0420     return QtConcurrent::run(WebManager::startProfile, pi);
0421 }
0422 
0423 QFuture<bool> stopProfile(const QSharedPointer<ProfileInfo> &pi)
0424 {
0425     return QtConcurrent::run(WebManager::stopProfile, pi);
0426 }
0427 }
0428 
0429 }