Kstars

servermanager.cpp
1/*
2 SPDX-FileCopyrightText: 2012 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include "servermanager.h"
8
9#include "driverinfo.h"
10#include "clientmanager.h"
11#include "drivermanager.h"
12#include "auxiliary/kspaths.h"
13#include "auxiliary/ksmessagebox.h"
14#include "Options.h"
15#include "ksnotification.h"
16
17#include <indidevapi.h>
18#include <thread>
19
20#include <KMessageBox>
21#include <QUuid>
22
23#include <sys/stat.h>
24
25#include <indi_debug.h>
26
27// Qt version calming
28#include <qtendl.h>
29
30ServerManager::ServerManager(const QString &inHost, int inPort) : host(inHost), port(inPort)
31{
32 connect(this, &ServerManager::scriptDriverStarted, this, &ServerManager::connectScriptDriver, Qt::BlockingQueuedConnection);
33}
34
35ServerManager::~ServerManager()
36{
37 serverSocket.close();
38 indiFIFO.close();
39
40 QFile::remove(indiFIFO.fileName());
41
42 if (serverProcess.get() != nullptr)
43 serverProcess->close();
44}
45
46bool ServerManager::start()
47{
48#ifdef Q_OS_WIN
49 qWarning() << "INDI server is currently not supported on Windows.";
50 return false;
51#else
52 bool connected = false;
53
54 if (serverProcess.get() == nullptr)
55 {
56 serverBuffer.open();
57
58 serverProcess.reset(new QProcess(this));
59#ifdef Q_OS_MACOS
60 QString driversDir = Options::indiDriversDir();
61 if (Options::indiDriversAreInternal())
62 driversDir = QCoreApplication::applicationDirPath() + "/../Resources/DriverSupport";
63 QString indiServerDir;
64 if (Options::indiServerIsInternal())
66 else
67 indiServerDir = QFileInfo(Options::indiServer()).dir().path();
68 QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
69 env.insert("PATH", driversDir + ':' + indiServerDir + ":/usr/local/bin:/usr/bin:/bin");
70 QString gscDirPath = QDir(KSPaths::writableLocation(QStandardPaths::AppLocalDataLocation)).filePath("gsc");
71 env.insert("GSCDAT", gscDirPath);
72
73 insertEnvironmentPath(&env, "INDIPREFIX", "/../../");
74 insertEnvironmentPath(&env, "IOLIBS", "/../Resources/DriverSupport/gphoto/IOLIBS");
75 insertEnvironmentPath(&env, "CAMLIBS", "/../Resources/DriverSupport/gphoto/CAMLIBS");
76
77 serverProcess->setProcessEnvironment(env);
78#endif
79 }
80
81 QStringList args;
82
83 args << "-v" << "-p" << QString::number(port);
84
85 QString fifoFile = QString("/tmp/indififo%1").arg(QUuid::createUuid().toString().mid(1, 8));
86
87 if (mkfifo(fifoFile.toLatin1(), S_IRUSR | S_IWUSR) < 0)
88 {
89 emit failed(i18n("Error making FIFO file %1: %2.", fifoFile, strerror(errno)));
90 return false;
91 }
92
93 indiFIFO.setFileName(fifoFile);
94
95 if (!indiFIFO.open(QIODevice::ReadWrite | QIODevice::Text))
96 {
97 qCCritical(KSTARS_INDI) << "Unable to create INDI FIFO file: " << fifoFile;
98 emit failed(i18n("Unable to create INDI FIFO file %1", fifoFile));
99 return false;
100 }
101
102 args << "-m" << QString::number(Options::serverTransferBufferSize()) << "-r" << "0" << "-f" << fifoFile;
103
104 qCDebug(KSTARS_INDI) << "Starting INDI Server: " << args << "-f" << fifoFile;
105
106 serverProcess->setProcessChannelMode(QProcess::SeparateChannels);
107 serverProcess->setReadChannel(QProcess::StandardError);
108
109#ifdef Q_OS_MACOS
110 if (Options::indiServerIsInternal())
111 serverProcess->start(QCoreApplication::applicationDirPath() + "/indiserver", args);
112 else
113#endif
114 serverProcess->start(Options::indiServer(), args);
115
116 connected = serverProcess->waitForStarted();
117
118 if (connected)
119 {
120 connect(serverProcess.get(), &QProcess::errorOccurred, this, &ServerManager::processServerError);
121 connect(serverProcess.get(), &QProcess::readyReadStandardError, this, &ServerManager::processStandardError);
122 emit started();
123 }
124 else
125 {
126 emit failed(i18n("INDI server failed to start: %1", serverProcess->errorString()));
127 }
128
129 qCDebug(KSTARS_INDI) << "INDI Server Started? " << connected;
130
131 return connected;
132#endif
133}
134
135void ServerManager::insertEnvironmentPath(QProcessEnvironment *env, const QString &variable, const QString &relativePath)
136{
137 QString environmentPath = QCoreApplication::applicationDirPath() + relativePath;
138 if (QFileInfo::exists(environmentPath) && Options::indiDriversAreInternal())
139 env->insert(variable, QDir(environmentPath).absolutePath());
140}
141
142void ServerManager::startDriver(const QSharedPointer<DriverInfo> &driver)
143{
144 QTextStream out(&indiFIFO);
145
146 // Check for duplicates within existing clients
147 if (driver->getUniqueLabel().isEmpty() && driver->getLabel().isEmpty() == false)
148 driver->setUniqueLabel(DriverManager::Instance()->getUniqueDeviceLabel(driver->getLabel()));
149
150 // Check for duplicates within managed drivers
151 if (driver->getUniqueLabel().isEmpty() == false)
152 {
153 QString uniqueLabel;
154 QString label = driver->getUniqueLabel();
155 int nset = 0;
156 {
157 QMutexLocker locker(&m_DriverMutex);
158 nset = std::count_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(), [label](auto & oneDriver)
159 {
160 return label == oneDriver->getUniqueLabel();
161 });
162 }
163 if (nset > 0)
164 {
165 uniqueLabel = QString("%1 %2").arg(label).arg(nset + 1);
166 driver->setUniqueLabel(uniqueLabel);
167 }
168 }
169
170 {
171 QMutexLocker locker(&m_DriverMutex);
172 m_ManagedDrivers.append(driver);
173 driver->setServerManager(this);
174 }
175
176 QString driversDir = Options::indiDriversDir();
177 QString indiServerDir = QFileInfo(Options::indiServer()).dir().path();
178
179#ifdef Q_OS_MACOS
180 if (Options::indiServerIsInternal())
181 indiServerDir = QCoreApplication::applicationDirPath();
182 if (Options::indiDriversAreInternal())
183 driversDir = QCoreApplication::applicationDirPath() + "/../Resources/DriverSupport";
184#endif
185
186 QJsonObject startupRule = driver->startupRule();
187 auto PreDelay = startupRule["PreDelay"].toInt(0);
188
189 // Sleep for PreDelay seconds if required.
190 if (PreDelay > 0)
191 {
192 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing pre-driver delay for" << PreDelay << "second(s)";
193 std::this_thread::sleep_for(std::chrono::seconds(PreDelay));
194 }
195
196 // Startup Script?
197 auto PreScript = startupRule["PreScript"].toString();
198 if (!PreScript.isEmpty())
199 {
200 QProcess script;
201 QEventLoop loop;
202 QObject::connect(&script, static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
203 &loop, &QEventLoop::quit);
205 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing pre-driver script" << PreScript;
206 script.start(PreScript, QStringList());
207 loop.exec();
208
209 if (script.exitCode() != 0)
210 {
211 emit driverFailed(driver, i18n("Pre driver startup script failed with exit code: %1", script.exitCode()));
212 return;
213 }
214 }
215
216 // Remote host?
217 if (driver->getRemoteHost().isEmpty() == false)
218 {
219 QString driverString = driver->getName() + "@" + driver->getRemoteHost() + ":" + driver->getRemotePort();
220 qCDebug(KSTARS_INDI) << "Starting Remote INDI Driver" << driverString;
221 out << "start " << driverString << Qt::endl;
222 out.flush();
223 }
224 // Local?
225 else
226 {
227 QStringList paths;
228 paths << "/usr/bin"
229 << "/usr/local/bin" << driversDir << indiServerDir;
230
231 if (QStandardPaths::findExecutable(driver->getExecutable()).isEmpty())
232 {
233 if (QStandardPaths::findExecutable(driver->getExecutable(), paths).isEmpty())
234 {
235 emit driverFailed(driver, i18n("Driver %1 was not found on the system. Please make sure the package that "
236 "provides the '%1' binary is installed.",
237 driver->getExecutable()));
238 return;
239 }
240 }
241
242 qCDebug(KSTARS_INDI) << "Starting INDI Driver" << driver->getExecutable();
243
244 out << "start " << driver->getExecutable();
245 if (driver->getUniqueLabel().isEmpty() == false)
246 out << " -n \"" << driver->getUniqueLabel() << "\"";
247 if (driver->getSkeletonFile().isEmpty() == false)
248 out << " -s \"" << driversDir << QDir::separator() << driver->getSkeletonFile() << "\"";
249 out << Qt::endl;
250 out.flush();
251
252 driver->setServerState(true);
253
254 driver->setPort(port);
255 }
256
257 auto PostDelay = startupRule["PostDelay"].toInt(0);
258
259 // Sleep for PostDelay seconds if required.
260 if (PostDelay > 0)
261 {
262 emit scriptDriverStarted(driver);
263 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing post-driver delay for" << PreDelay << "second(s)";
264 std::this_thread::sleep_for(std::chrono::seconds(PostDelay));
265 }
266
267 // Startup Script?
268 auto PostScript = startupRule["PostScript"].toString();
269 if (!PostScript.isEmpty())
270 {
271 QProcess script;
272 QEventLoop loop;
273 QObject::connect(&script, static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
274 &loop, &QEventLoop::quit);
276 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing post-driver script" << PreScript;
277 script.start(PostScript, QStringList());
278 loop.exec();
279
280 if (script.exitCode() != 0)
281 {
282 emit driverFailed(driver, i18n("Post driver startup script failed with exit code: %1", script.exitCode()));
283 return;
284 }
285 }
286
287 // Remove driver from pending list.
288 {
289 QMutexLocker locker(&m_PendingMutex);
290 m_PendingDrivers.erase(std::remove_if(m_PendingDrivers.begin(), m_PendingDrivers.end(), [driver](const auto & oneDriver)
291 {
292 return driver == oneDriver;
293 }), m_PendingDrivers.end());
294 }
295 emit driverStarted(driver);
296}
297
298void ServerManager::stopDriver(const QSharedPointer<DriverInfo> &driver)
299{
300 QTextStream out(&indiFIFO);
301 const auto exec = driver->getExecutable();
302
303 qCDebug(KSTARS_INDI) << "Stopping INDI Driver " << exec;
304
305 if (driver->getUniqueLabel().isEmpty() == false)
306 out << "stop " << exec << " -n \"" << driver->getUniqueLabel() << "\"";
307 else
308 out << "stop " << exec;
309 out << Qt::endl;
310 out.flush();
311 driver->setServerState(false);
312 driver->setPort(driver->getUserPort());
313
314 {
315 QMutexLocker locker(&m_DriverMutex);
316 m_ManagedDrivers.erase(std::remove_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(), [exec](const auto & driver)
317 {
318 return driver->getExecutable() == exec;
319 }));
320 }
321 emit driverStopped(driver);
322}
323
324
325bool ServerManager::restartDriver(const QSharedPointer<DriverInfo> &driver)
326{
327 auto cm = driver->getClientManager();
328 const auto label = driver->getLabel();
329
330 if (cm)
331 {
332 qCDebug(KSTARS_INDI) << "Restarting INDI Driver: " << label;
333 // N.B. This MUST be called BEFORE stopping driver below
334 // Since it requires the driver device pointer.
335 cm->removeManagedDriver(driver);
336
337 // Stop driver.
338 stopDriver(driver);
339 }
340 else
341 {
342 int size = 0;
343 {
344 QMutexLocker locker(&m_DriverMutex);
345 size = m_ManagedDrivers.size();
346 }
347 qCDebug(KSTARS_INDI) << "restartDriver with no cm, and " << size << " drivers. Trying to remove: " << label;
348 cm = DriverManager::Instance()->getClientManager(driver);
349 const auto exec = driver->getExecutable();
350 {
351 QMutexLocker locker(&m_DriverMutex);
352 m_ManagedDrivers.erase(std::remove_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(), [exec](const auto & driver)
353 {
354 return driver->getExecutable() == exec;
355 }));
356 }
357 }
358
359 // Wait 1 second before starting the driver again.
360 QTimer::singleShot(1000, this, [this, label, cm]()
361 {
362 auto driver = DriverManager::Instance()->findDriverByLabel(label);
363 if (!driver)
364 {
365 qCDebug(KSTARS_INDI) << "restartDriver timer, did not find driver with label: " << label;
366 return;
367 }
368 cm->appendManagedDriver(driver);
369 {
370 QMutexLocker locker(&m_DriverMutex);
371 if (m_ManagedDrivers.contains(driver) == false)
372 m_ManagedDrivers.append(driver);
373 }
374 driver->setServerManager(this);
375
376 QTextStream out(&indiFIFO);
377
378 QString driversDir = Options::indiDriversDir();
379 QString indiServerDir = Options::indiServer();
380
381#ifdef Q_OS_MACOS
382 if (Options::indiServerIsInternal())
383 indiServerDir = QCoreApplication::applicationDirPath();
384 if (Options::indiDriversAreInternal())
385 driversDir = QCoreApplication::applicationDirPath() + "/../Resources/DriverSupport";
386 else
387 indiServerDir = QFileInfo(Options::indiServer()).dir().path();
388#endif
389
390 if (driver->getRemoteHost().isEmpty() == false)
391 {
392 QString driverString = driver->getName() + "@" + driver->getRemoteHost() + ":" + driver->getRemotePort();
393 qCDebug(KSTARS_INDI) << "Restarting Remote INDI Driver" << driverString;
394 out << "start " << driverString;
395#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
396 out << Qt::endl;
397#else
398 out << Qt::endl;
399#endif
400 out.flush();
401 }
402 else
403 {
404 QStringList paths;
405 paths << "/usr/bin"
406 << "/usr/local/bin" << driversDir << indiServerDir;
407
408 qCDebug(KSTARS_INDI) << "Starting INDI Driver " << driver->getExecutable();
409
410 out << "start " << driver->getExecutable();
411 if (driver->getUniqueLabel().isEmpty() == false)
412 out << " -n \"" << driver->getUniqueLabel() << "\"";
413 if (driver->getSkeletonFile().isEmpty() == false)
414 out << " -s \"" << driversDir << QDir::separator() << driver->getSkeletonFile() << "\"";
415 out << Qt::endl;
416 out.flush();
417
418 driver->setServerState(true);
419 driver->setPort(port);
420 }
421 });
422
423 emit driverRestarted(driver);
424 return true;
425}
426
427void ServerManager::stop()
428{
429 if (serverProcess.get() == nullptr)
430 return;
431
432 {
433 QMutexLocker locker(&m_DriverMutex);
434 for (auto &device : m_ManagedDrivers)
435 {
436 device->reset();
437 }
438 }
439
440 qCDebug(KSTARS_INDI) << "Stopping INDI Server " << host << "@" << port;
441
442 serverProcess->disconnect(this);
443
444 serverBuffer.close();
445
446 serverProcess->terminate();
447
448 serverProcess->waitForFinished();
449
450 serverProcess.reset();
451
452 indiFIFO.close();
453 QFile::remove(indiFIFO.fileName());
454 emit stopped();
455
456}
457
458void ServerManager::processServerError(QProcess::ProcessError err)
459{
460 Q_UNUSED(err)
461 emit terminated(i18n("Connection to INDI server %1:%2 terminated: %3.",
462 getHost(), getPort(), serverProcess.get()->errorString()));
463}
464
465void ServerManager::processStandardError()
466{
467#ifdef Q_OS_WIN
468 qWarning() << "INDI server is currently not supported on Windows.";
469 return;
470#else
471 QString stderr = serverProcess->readAllStandardError();
472
473 for (auto &msg : stderr.split('\n'))
474 qCDebug(KSTARS_INDI) << "INDI Server: " << msg;
475
476 serverBuffer.write(stderr.toLatin1());
477 emit newServerLog();
478
479 //if (driverCrashed == false && (stderr.contains("stdin EOF") || stderr.contains("stderr EOF")))
480 QRegularExpression re("Driver (.*): Terminated after #0 restarts");
481 QRegularExpressionMatch match = re.match(stderr);
482 if (match.hasMatch())
483 {
484 QString driverExec = match.captured(1);
485 qCCritical(KSTARS_INDI) << "INDI driver " << driverExec << " crashed!";
486
487 //KSNotification::info(i18n("KStars detected INDI driver %1 crashed. Please check INDI server log in the Device Manager.", driverName));
488
489 QSharedPointer<DriverInfo> crashedDriverInfo;
490 {
491 QMutexLocker locker(&m_DriverMutex);
492 auto crashedDriver = std::find_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(),
493 [driverExec](QSharedPointer<DriverInfo> dv)
494 {
495 return dv->getExecutable() == driverExec;
496 });
497
498 if (crashedDriver != m_ManagedDrivers.end())
499 crashedDriverInfo = *crashedDriver;
500 }
501
502 if (crashedDriverInfo)
503 {
504 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, crashedDriverInfo]()
505 {
506 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
507 KSMessageBox::Instance()->disconnect(this);
508 restartDriver(crashedDriverInfo);
509 });
510
511 QString label = crashedDriverInfo->getUniqueLabel();
512 if (label.isEmpty())
513 label = crashedDriverInfo->getExecutable();
514 KSMessageBox::Instance()->warningContinueCancel(i18n("INDI Driver <b>%1</b> crashed. Restart it?",
515 label), i18n("Driver crash"), 10);
516 }
517 }
518#endif
519}
520
521QString ServerManager::errorString()
522{
523 if (serverProcess.get() != nullptr)
524 return serverProcess->errorString();
525
526 return nullptr;
527}
528
529QString ServerManager::getLogBuffer()
530{
531 serverBuffer.flush();
532 serverBuffer.close();
533
534 serverBuffer.open();
535 return serverBuffer.readAll();
536}
537
538void ServerManager::connectScriptDriver(const QSharedPointer<DriverInfo> &driver)
539{
540 // If we have post delay, ensure all devices are connected for this driver
541 auto host = driver->getRemoteHost().isEmpty() ? driver->getHost() : driver->getRemoteHost();
542 auto port = driver->getRemoteHost().isEmpty() ? driver->getPort() : driver->getRemotePort().toInt();
543 auto manager = new ClientManager();
544 manager->setServer(host.toLatin1().constData(), port);
545 connect(manager, &ClientManager::newINDIProperty, [manager](INDI::Property property)
546 {
547 if (QString(property.getName()) == "CONNECTION")
548 manager->connectDevice(property.getDeviceName());
549 });
550 manager->establishConnection();
551 // Destory after 5 seconds in all cases
552 QTimer::singleShot(5000, this, [manager]()
553 {
554 manager->disconnect();
555 manager->deleteLater();
556 });
557}
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString label(StandardShortcut id)
NETWORKMANAGERQT_EXPORT NetworkManager::Status status()
QString applicationDirPath()
void accepted()
QChar separator()
int exec(ProcessEventsFlags flags)
void quit()
bool remove()
bool exists() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QVariant property(const char *name) const const
void errorOccurred(QProcess::ProcessError error)
int exitCode() const const
void finished(int exitCode, QProcess::ExitStatus exitStatus)
void readyReadStandardError()
void start(OpenMode mode)
void insert(const QProcessEnvironment &e)
QProcessEnvironment systemEnvironment()
QString findExecutable(const QString &executableName, const QStringList &paths)
QString arg(Args &&... args) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray toLatin1() const const
BlockingQueuedConnection
QTextStream & endl(QTextStream &stream)
void flush()
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUuid createUuid()
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:56:02 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.