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 startupShutdownRule = driver->startupShutdownRule();
187 auto PreDelay = startupShutdownRule["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 = startupShutdownRule["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 = startupShutdownRule["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 = startupShutdownRule["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 QJsonObject startupShutdownRule = driver->startupShutdownRule();
303
304 // Pre Shutdown Script?
305 auto StoppingScript = startupShutdownRule["StoppingScript"].toString();
306 if (!StoppingScript.isEmpty())
307 {
308 QProcess script;
309 QEventLoop loop;
310 QObject::connect(&script, static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
311 &loop, &QEventLoop::quit);
313 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing pre-shutdown driver script" << StoppingScript;
314 script.start(StoppingScript, QStringList());
315 loop.exec();
316
317 if (script.exitCode() != 0)
318 {
319 emit driverFailed(driver, i18n("Pre driver shutdown script failed with exit code: %1", script.exitCode()));
320 return;
321 }
322 }
323
324 auto StoppingDelay = startupShutdownRule["StoppingDelay"].toInt(0);
325
326 // Sleep for StoppingDelay seconds if required.
327 if (StoppingDelay > 0)
328 {
329 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing pre-driver shutdown delay for" << StoppingDelay << "second(s)";
330 std::this_thread::sleep_for(std::chrono::seconds(StoppingDelay));
331 }
332
333 qCDebug(KSTARS_INDI) << "Stopping INDI Driver " << exec;
334
335 if (driver->getUniqueLabel().isEmpty() == false)
336 out << "stop " << exec << " -n \"" << driver->getUniqueLabel() << "\"";
337 else
338 out << "stop " << exec;
339 out << Qt::endl;
340 out.flush();
341 driver->setServerState(false);
342 driver->setPort(driver->getUserPort());
343
344 {
345 QMutexLocker locker(&m_DriverMutex);
346 m_ManagedDrivers.erase(std::remove_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(), [exec](const auto & driver)
347 {
348 return driver->getExecutable() == exec;
349 }));
350 }
351
352 auto StoppedDelay = startupShutdownRule["StoppedDelay"].toInt(0);
353
354 // Sleep for StoppedDelay seconds if required.
355 if (StoppedDelay > 0)
356 {
357 emit scriptDriverStarted(driver);
358 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing post-driver shutdown delay for" << StoppedDelay << "second(s)";
359 std::this_thread::sleep_for(std::chrono::seconds(StoppedDelay));
360 }
361
362 // Post Startdown Script?
363 auto StoppedScript = startupShutdownRule["Stoppedcript"].toString();
364 if (!StoppedScript.isEmpty())
365 {
366 QProcess script;
367 QEventLoop loop;
368 QObject::connect(&script, static_cast<void (QProcess::*)(int exitCode, QProcess::ExitStatus status)>(&QProcess::finished),
369 &loop, &QEventLoop::quit);
371 qCDebug(KSTARS_INDI) << driver->getUniqueLabel() << ": Executing post-driver shutdown script" << StoppedScript;
372 script.start(StoppedScript, QStringList());
373 loop.exec();
374
375 if (script.exitCode() != 0)
376 {
377 emit driverFailed(driver, i18n("Post driver shutdown script failed with exit code: %1", script.exitCode()));
378 return;
379 }
380 }
381 emit driverStopped(driver);
382}
383
384
385bool ServerManager::restartDriver(const QSharedPointer<DriverInfo> &driver)
386{
387 auto cm = driver->getClientManager();
388 const auto label = driver->getLabel();
389
390 if (cm)
391 {
392 qCDebug(KSTARS_INDI) << "Restarting INDI Driver: " << label;
393 // N.B. This MUST be called BEFORE stopping driver below
394 // Since it requires the driver device pointer.
395 cm->removeManagedDriver(driver);
396
397 // Stop driver.
398 stopDriver(driver);
399 }
400 else
401 {
402 int size = 0;
403 {
404 QMutexLocker locker(&m_DriverMutex);
405 size = m_ManagedDrivers.size();
406 }
407 qCDebug(KSTARS_INDI) << "restartDriver with no cm, and " << size << " drivers. Trying to remove: " << label;
408 cm = DriverManager::Instance()->getClientManager(driver);
409 const auto exec = driver->getExecutable();
410 {
411 QMutexLocker locker(&m_DriverMutex);
412 m_ManagedDrivers.erase(std::remove_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(), [exec](const auto & driver)
413 {
414 return driver->getExecutable() == exec;
415 }));
416 }
417 }
418
419 // Wait 1 second before starting the driver again.
420 QTimer::singleShot(1000, this, [this, label, cm]()
421 {
422 auto driver = DriverManager::Instance()->findDriverByLabel(label);
423 if (!driver)
424 {
425 qCDebug(KSTARS_INDI) << "restartDriver timer, did not find driver with label: " << label;
426 return;
427 }
428 cm->appendManagedDriver(driver);
429 {
430 QMutexLocker locker(&m_DriverMutex);
431 if (m_ManagedDrivers.contains(driver) == false)
432 m_ManagedDrivers.append(driver);
433 }
434 driver->setServerManager(this);
435
436 QTextStream out(&indiFIFO);
437
438 QString driversDir = Options::indiDriversDir();
439 QString indiServerDir = Options::indiServer();
440
441#ifdef Q_OS_MACOS
442 if (Options::indiServerIsInternal())
443 indiServerDir = QCoreApplication::applicationDirPath();
444 if (Options::indiDriversAreInternal())
445 driversDir = QCoreApplication::applicationDirPath() + "/../Resources/DriverSupport";
446 else
447 indiServerDir = QFileInfo(Options::indiServer()).dir().path();
448#endif
449
450 if (driver->getRemoteHost().isEmpty() == false)
451 {
452 QString driverString = driver->getName() + "@" + driver->getRemoteHost() + ":" + driver->getRemotePort();
453 qCDebug(KSTARS_INDI) << "Restarting Remote INDI Driver" << driverString;
454 out << "start " << driverString;
455#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
456 out << Qt::endl;
457#else
458 out << Qt::endl;
459#endif
460 out.flush();
461 }
462 else
463 {
464 QStringList paths;
465 paths << "/usr/bin"
466 << "/usr/local/bin" << driversDir << indiServerDir;
467
468 qCDebug(KSTARS_INDI) << "Starting INDI Driver " << driver->getExecutable();
469
470 out << "start " << driver->getExecutable();
471 if (driver->getUniqueLabel().isEmpty() == false)
472 out << " -n \"" << driver->getUniqueLabel() << "\"";
473 if (driver->getSkeletonFile().isEmpty() == false)
474 out << " -s \"" << driversDir << QDir::separator() << driver->getSkeletonFile() << "\"";
475 out << Qt::endl;
476 out.flush();
477
478 driver->setServerState(true);
479 driver->setPort(port);
480 }
481 });
482
483 emit driverRestarted(driver);
484 return true;
485}
486
487void ServerManager::stop()
488{
489 if (serverProcess.get() == nullptr)
490 return;
491
492 {
493 QMutexLocker locker(&m_DriverMutex);
494 for (auto &device : m_ManagedDrivers)
495 {
496 device->reset();
497 }
498 }
499
500 qCDebug(KSTARS_INDI) << "Stopping INDI Server " << host << "@" << port;
501
502 serverProcess->disconnect(this);
503
504 serverBuffer.close();
505
506 serverProcess->terminate();
507
508 serverProcess->waitForFinished();
509
510 serverProcess.reset();
511
512 indiFIFO.close();
513 QFile::remove(indiFIFO.fileName());
514 emit stopped();
515
516}
517
518void ServerManager::processServerError(QProcess::ProcessError err)
519{
520 Q_UNUSED(err)
521 emit terminated(i18n("Connection to INDI server %1:%2 terminated: %3.",
522 getHost(), getPort(), serverProcess.get()->errorString()));
523}
524
525void ServerManager::processStandardError()
526{
527#ifdef Q_OS_WIN
528 qWarning() << "INDI server is currently not supported on Windows.";
529 return;
530#else
531 QString stderr = serverProcess->readAllStandardError();
532
533 for (auto &msg : stderr.split('\n'))
534 qCDebug(KSTARS_INDI) << "INDI Server: " << msg;
535
536 serverBuffer.write(stderr.toLatin1());
537 emit newServerLog();
538
539 //if (driverCrashed == false && (stderr.contains("stdin EOF") || stderr.contains("stderr EOF")))
540 QRegularExpression re("Driver (.*): Terminated after #0 restarts");
541 QRegularExpressionMatch match = re.match(stderr);
542 if (match.hasMatch())
543 {
544 QString driverExec = match.captured(1);
545 qCCritical(KSTARS_INDI) << "INDI driver " << driverExec << " crashed!";
546
547 //KSNotification::info(i18n("KStars detected INDI driver %1 crashed. Please check INDI server log in the Device Manager.", driverName));
548
549 QSharedPointer<DriverInfo> crashedDriverInfo;
550 {
551 QMutexLocker locker(&m_DriverMutex);
552 auto crashedDriver = std::find_if(m_ManagedDrivers.begin(), m_ManagedDrivers.end(),
553 [driverExec](QSharedPointer<DriverInfo> dv)
554 {
555 return dv->getExecutable() == driverExec;
556 });
557
558 if (crashedDriver != m_ManagedDrivers.end())
559 crashedDriverInfo = *crashedDriver;
560 }
561
562 if (crashedDriverInfo)
563 {
564 connect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, [this, crashedDriverInfo]()
565 {
566 //QObject::disconnect(KSMessageBox::Instance(), &KSMessageBox::accepted, this, nullptr);
567 KSMessageBox::Instance()->disconnect(this);
568 restartDriver(crashedDriverInfo);
569 });
570
571 QString label = crashedDriverInfo->getUniqueLabel();
572 if (label.isEmpty())
573 label = crashedDriverInfo->getExecutable();
574 KSMessageBox::Instance()->warningContinueCancel(i18n("INDI Driver <b>%1</b> crashed. Restart it?",
575 label), i18n("Driver crash"), 10);
576 }
577 }
578#endif
579}
580
581QString ServerManager::errorString()
582{
583 if (serverProcess.get() != nullptr)
584 return serverProcess->errorString();
585
586 return nullptr;
587}
588
589QString ServerManager::getLogBuffer()
590{
591 serverBuffer.flush();
592 serverBuffer.close();
593
594 serverBuffer.open();
595 return serverBuffer.readAll();
596}
597
598void ServerManager::connectScriptDriver(const QSharedPointer<DriverInfo> &driver)
599{
600 // If we have post delay, ensure all devices are connected for this driver
601 auto host = driver->getRemoteHost().isEmpty() ? driver->getHost() : driver->getRemoteHost();
602 auto port = driver->getRemoteHost().isEmpty() ? driver->getPort() : driver->getRemotePort().toInt();
603 auto manager = new ClientManager();
604 manager->setServer(host.toLatin1().constData(), port);
605 connect(manager, &ClientManager::newINDIProperty, [manager](INDI::Property property)
606 {
607 if (QString(property.getName()) == "CONNECTION")
608 manager->connectDevice(property.getDeviceName());
609 });
610 manager->establishConnection();
611 // Destory after 5 seconds in all cases
612 QTimer::singleShot(5000, this, [manager]()
613 {
614 manager->disconnect();
615 manager->deleteLater();
616 });
617}
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 Apr 25 2025 11:58:37 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.