File indexing completed on 2024-03-24 15:17:20
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 }