1 /*
2     SPDX-FileCopyrightText: 2016 Jasem Mutlaq <mutlaqja@ikarustech.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include "indiwebmanager.h"
8 
9 #include "auxiliary/ksnotification.h"
10 #include "driverinfo.h"
11 #include "drivermanager.h"
12 #include "Options.h"
13 #include "profileinfo.h"
14 
15 #include <QJsonArray>
16 #include <QJsonDocument>
17 #include <QJsonObject>
18 #include <QNetworkReply>
19 #include <QtConcurrent>
20 
21 #include "ekos_debug.h"
22 
23 namespace INDI
24 {
25 
26 namespace WebManager
27 {
getWebManagerResponse(QNetworkAccessManager::Operation operation,const QUrl & url,QJsonDocument * reply,QByteArray * data)28 bool getWebManagerResponse(QNetworkAccessManager::Operation operation, const QUrl &url, QJsonDocument *reply,
29                            QByteArray *data)
30 {
31     QNetworkAccessManager manager;
32     QNetworkReply *response = nullptr;
33     QNetworkRequest request;
34 
35     request.setUrl(url);
36 
37     if (data)
38     {
39         request.setRawHeader("Content-Type", "application/json");
40         request.setRawHeader("Content-Length", QByteArray::number(data->size()));
41     }
42 
43     switch (operation)
44     {
45         case QNetworkAccessManager::GetOperation:
46             response = manager.get(request);
47             break;
48 
49         case QNetworkAccessManager::PostOperation:
50             if (data)
51                 response = manager.post(request, *data);
52             else
53                 response = manager.post(request, QByteArray());
54             break;
55 
56         case QNetworkAccessManager::DeleteOperation:
57             response = manager.deleteResource(request);
58             break;
59 
60         case QNetworkAccessManager::PutOperation:
61             response = manager.put(request, *data);
62             break;
63 
64         default:
65             return false;
66     }
67 
68     // Wait synchronously
69     QEventLoop event;
70     QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
71     event.exec();
72 
73     if (response->error() == QNetworkReply::NoError)
74     {
75         if (reply)
76         {
77             QJsonParseError parseError;
78             *reply = QJsonDocument::fromJson(response->readAll(), &parseError);
79 
80             if (parseError.error != QJsonParseError::NoError)
81             {
82                 qDebug() << "INDI: JSon error during parsing " << parseError.errorString();
83                 return false;
84             }
85         }
86 
87         return true;
88     }
89     else
90     {
91         qDebug() << "INDI: Error communicating with INDI Web Manager: " << response->errorString();
92         return false;
93     }
94 }
95 
isOnline(ProfileInfo * pi)96 bool isOnline(ProfileInfo *pi)
97 {
98     QTimer timer;
99     timer.setSingleShot(true);
100     QNetworkAccessManager manager;
101     QUrl url(QString("http://%1:%2/api/server/status").arg(pi->host).arg(pi->INDIWebManagerPort));
102     QNetworkReply *response = manager.get(QNetworkRequest(url));
103 
104     // Wait synchronously
105     QEventLoop event;
106     QObject::connect(&timer, &QTimer::timeout, &event, &QEventLoop::quit);
107     QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
108     timer.start(3000);
109     event.exec();
110 
111     if (timer.isActive() && response->error() == QNetworkReply::NoError)
112         return true;
113     // Fallback to default if DNS lookup fails for .local
114     else if (pi->host.contains(".local"))
115     {
116         QUrl url(QString("http://10.250.250.1:8624/api/server/status"));
117         QNetworkReply *response = manager.get(QNetworkRequest(url));
118         // Wait synchronously
119         QEventLoop event;
120         QObject::connect(&timer, &QTimer::timeout, &event, &QEventLoop::quit);
121         QObject::connect(response, SIGNAL(finished()), &event, SLOT(quit()));
122         timer.start(3000);
123         event.exec();
124 
125         if (timer.isActive() && response->error() == QNetworkReply::NoError)
126         {
127             pi->host = "10.250.250.1";
128             return true;
129         }
130     }
131 
132     return false;
133 }
134 
isStellarMate(ProfileInfo * pi)135 bool isStellarMate(ProfileInfo *pi)
136 {
137     QNetworkAccessManager manager;
138     QUrl url(QString("http://%1:%2/api/info/version").arg(pi->host).arg(pi->INDIWebManagerPort));
139 
140     QJsonDocument json;
141     if (getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
142     {
143         QJsonObject version = json.object();
144         if (version.contains("version") == false)
145             return false;
146         qInfo(KSTARS_EKOS) << "Detect StellarMate version" << version["version"].toString();
147         return true;
148     }
149     return false;
150 }
151 
syncCustomDrivers(ProfileInfo * pi)152 bool syncCustomDrivers(ProfileInfo *pi)
153 {
154     QNetworkAccessManager manager;
155     QUrl url(QString("http://%1:%2/api/profiles/custom").arg(pi->host).arg(pi->INDIWebManagerPort));
156 
157     QStringList customDriversLabels;
158     QMapIterator<QString, QString> i(pi->drivers);
159     while (i.hasNext())
160     {
161         QString name       = i.next().value();
162         DriverInfo *driver = DriverManager::Instance()->findDriverByName(name);
163 
164         if (driver == nullptr)
165             driver = DriverManager::Instance()->findDriverByLabel(name);
166         if (driver && driver->getDriverSource() == CUSTOM_SOURCE)
167             customDriversLabels << driver->getLabel();
168     }
169 
170     // Search for locked filter by filter color name
171     const QList<QVariantMap> &customDrivers = DriverManager::Instance()->getCustomDrivers();
172 
173     for (auto label : customDriversLabels)
174     {
175         auto pos = std::find_if(customDrivers.begin(), customDrivers.end(), [label](QVariantMap oneDriver)
176         {
177             return (oneDriver["Label"] == label);
178         });
179 
180         if (pos == customDrivers.end())
181             continue;
182 
183         QVariantMap driver = (*pos);
184         QJsonObject jsonDriver = QJsonObject::fromVariantMap(driver);
185 
186         QByteArray data    = QJsonDocument(jsonDriver).toJson();
187         getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
188     }
189 
190     return true;
191 }
192 
areDriversRunning(ProfileInfo * pi)193 bool areDriversRunning(ProfileInfo *pi)
194 {
195     QUrl url(QString("http://%1:%2/api/server/drivers").arg(pi->host).arg(pi->INDIWebManagerPort));
196     QJsonDocument json;
197 
198     if (getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
199     {
200         QJsonArray array = json.array();
201 
202         if (array.isEmpty())
203             return false;
204 
205         QStringList piExecDrivers;
206         QMapIterator<QString, QString> i(pi->drivers);
207         while (i.hasNext())
208         {
209             QString name       = i.next().value();
210             DriverInfo *driver = DriverManager::Instance()->findDriverByName(name);
211 
212             if (driver == nullptr)
213                 driver = DriverManager::Instance()->findDriverByLabel(name);
214             if (driver)
215                 piExecDrivers << driver->getExecutable();
216         }
217 
218         if (array.count() < piExecDrivers.count())
219             return false;
220 
221         // Get all the drivers running remotely
222         QStringList webManagerDrivers;
223         for (auto value : array)
224         {
225             QJsonObject driver = value.toObject();
226             // Old Web Manager API API
227             QString exec = driver["driver"].toString();
228             if (exec.isEmpty())
229                 // New v0.1.5+ Web Manager API
230                 exec = driver["binary"].toString();
231             webManagerDrivers << exec;
232         }
233 
234         // Make sure all the profile drivers are running there
235         for (auto &oneDriverExec : piExecDrivers)
236         {
237             if (webManagerDrivers.contains(oneDriverExec) == false)
238             {
239                 KSNotification::error(i18n("Driver %1 failed to start on the remote INDI server.", oneDriverExec));
240                 qCritical(KSTARS_EKOS) << "Driver" << oneDriverExec << "failed to start on the remote INDI server!";
241                 return false;
242             }
243         }
244 
245         return true;
246     }
247 
248     return false;
249 }
250 
syncProfile(ProfileInfo * pi)251 bool syncProfile(ProfileInfo *pi)
252 {
253     QUrl url;
254     QJsonDocument jsonDoc;
255     QByteArray data;
256 
257     //Add Profile
258     url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
259     getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
260 
261     // Update profile info
262     url = QUrl(QString("http://%1:%2/api/profiles/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
263     QJsonObject profileObject{ { "port", pi->port } };
264     jsonDoc = QJsonDocument(profileObject);
265     data    = jsonDoc.toJson();
266     getWebManagerResponse(QNetworkAccessManager::PutOperation, url, nullptr, &data);
267 
268     // Add drivers
269     url = QUrl(QString("http://%1:%2/api/profiles/%3/drivers").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
270     QJsonArray driverArray;
271     QMapIterator<QString, QString> i(pi->drivers);
272 
273     // In case both Guider + CCD are Multiple-Devices-Per-Driver type
274     // Then we should not define guider as a separate driver since that would start the driver executable twice
275     // when we only need it once
276     if (pi->drivers.contains("Guider"))
277     {
278         if (pi->drivers["Guider"] == pi->drivers["CCD"])
279         {
280             DriverInfo *guiderInfo = nullptr;
281             if ((guiderInfo = DriverManager::Instance()->findDriverByName(pi->drivers["Guider"])) == nullptr)
282             {
283                 if ((guiderInfo = DriverManager::Instance()->findDriverByLabel(pi->drivers["Guider"])) == nullptr)
284                 {
285                     guiderInfo = DriverManager::Instance()->findDriverByExec(pi->drivers["Guider"]);
286                 }
287             }
288 
289             if (guiderInfo && guiderInfo->getAuxInfo().value("mdpd", false).toBool())
290             {
291                 pi->drivers.remove("Guider");
292                 i = QMapIterator<QString, QString>(pi->drivers);
293             }
294         }
295     }
296 
297     // Regular Drivers
298     while (i.hasNext())
299         driverArray.append(QJsonObject({{"label", i.next().value()}}));
300 
301     // Remote Drivers
302     if (pi->remotedrivers.isEmpty() == false)
303     {
304         for (auto remoteDriver : pi->remotedrivers.split(","))
305         {
306             driverArray.append(QJsonObject({{"remote", remoteDriver}}));
307         }
308     }
309 
310     data    = QJsonDocument(driverArray).toJson();
311     getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data);
312 
313     return true;
314 }
315 
startProfile(ProfileInfo * pi)316 bool startProfile(ProfileInfo *pi)
317 {
318     // First make sure profile is created and synced on web manager
319     syncProfile(pi);
320 
321     // Start profile
322     QUrl url(QString("http://%1:%2/api/server/start/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(pi->name));
323     getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
324 
325     // Make sure drivers are running
326     // Try up to 3 times
327     for (int i = 0; i < 3; i++)
328     {
329         if (areDriversRunning(pi))
330             return true;
331     }
332 
333     return false;
334 }
335 
stopProfile(ProfileInfo * pi)336 bool stopProfile(ProfileInfo *pi)
337 {
338     // Stop profile
339     QUrl url(QString("http://%1:%2/api/server/stop").arg(pi->host).arg(pi->INDIWebManagerPort));
340     return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
341 }
342 
restartDriver(ProfileInfo * pi,const QString & label)343 bool restartDriver(ProfileInfo *pi, const QString &label)
344 {
345     QUrl url(QString("http://%1:%2/api/drivers/restart/%3").arg(pi->host).arg(pi->INDIWebManagerPort).arg(label));
346     return getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr);
347 }
348 }
349 
350 // Async version of the Web Manager
351 namespace AsyncWebManager
352 {
353 
isOnline(ProfileInfo * pi)354 QFuture<bool> isOnline(ProfileInfo *pi)
355 {
356     return QtConcurrent::run(WebManager::isOnline, pi);
357 }
358 
isStellarMate(ProfileInfo * pi)359 QFuture<bool> isStellarMate(ProfileInfo *pi)
360 {
361     return QtConcurrent::run(WebManager::isStellarMate, pi);
362 }
363 
syncCustomDrivers(ProfileInfo * pi)364 QFuture<bool> syncCustomDrivers(ProfileInfo *pi)
365 {
366     return QtConcurrent::run(WebManager::syncCustomDrivers, pi);
367 }
368 
areDriversRunning(ProfileInfo * pi)369 QFuture<bool> areDriversRunning(ProfileInfo *pi)
370 {
371     return QtConcurrent::run(WebManager::areDriversRunning, pi);
372 }
373 
syncProfile(ProfileInfo * pi)374 QFuture<bool> syncProfile(ProfileInfo *pi)
375 {
376     return QtConcurrent::run(WebManager::syncProfile, pi);
377 }
378 
startProfile(ProfileInfo * pi)379 QFuture<bool> startProfile(ProfileInfo *pi)
380 {
381     return QtConcurrent::run(WebManager::startProfile, pi);
382 }
383 
stopProfile(ProfileInfo * pi)384 QFuture<bool> stopProfile(ProfileInfo *pi)
385 {
386     return QtConcurrent::run(WebManager::stopProfile, pi);
387 }
388 }
389 
390 }
391