nymea-plugins/tmate/integrationplugintmate.cpp

192 lines
7.3 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-plugins.
*
* nymea-plugins is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-plugins is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-plugins. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationplugintmate.h"
#include "plugininfo.h"
#include <plugintimer.h>
#include <QFile>
#include <QDir>
#include <QProcess>
#include <QRegularExpression>
IntegrationPluginTmate::IntegrationPluginTmate()
{
}
IntegrationPluginTmate::~IntegrationPluginTmate()
{
foreach (QProcess *process, m_processes) {
process->terminate();
}
}
void IntegrationPluginTmate::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
QStringList arguments;
QString apiKey = thing->paramValue(tmateThingApiKeyParamTypeId).toString();
QString sessionName = thing->paramValue(tmateThingSessionNameParamTypeId).toString();
arguments << "-F";
if (!apiKey.isEmpty()) {
arguments << "-k" << apiKey;
if (!sessionName.isEmpty()) {
arguments << "-n" << sessionName;
arguments << "-r" << "ro-" + sessionName;
}
}
QProcess *process = new QProcess(thing);
process->setProgram("tmate");
process->setArguments(arguments);
process->setProcessChannelMode(QProcess::MergedChannels);
m_processes.insert(info->thing(), process);
connect(process, &QProcess::stateChanged, thing, [=](QProcess::ProcessState newState){
switch (newState) {
case QProcess::Starting:
qCDebug(dcTmate()) << "Connection starting for" << thing->name();
return ;
case QProcess::Running:
qCInfo(dcTmate()) << "Reverse SSH connected for" << thing->name();
thing->setStateValue(tmateConnectedStateTypeId, true);
return;
case QProcess::NotRunning:
qCInfo(dcTmate()) << "Reverse SSH disconnected for" << thing->name();
thing->setStateValue(tmateConnectedStateTypeId, false);
thing->setStateValue(tmateSshStateTypeId, "");
thing->setStateValue(tmateSshRoStateTypeId, "");
thing->setStateValue(tmateWebStateTypeId, "");
thing->setStateValue(tmateWebRoStateTypeId, "");
thing->setStateValue(tmateClientsStateTypeId, 0);
return;
}
});
connect(process, &QProcess::readyRead, thing, [=](){
while (process->canReadLine()) {
QByteArray data = process->readLine();
qCDebug(dcTmate()) << thing->name() << ":" << data;
auto extractSession = [thing](const StateTypeId &stateTypeId, const QString &type, const QString &input) {
int sessionStart = input.indexOf(type);
if (sessionStart >= 0) {
int sessionEnd = input.indexOf(QChar('\n'), sessionStart);
qCInfo(dcTmate()) << input << "Session start" << sessionStart << "session end" << sessionEnd;
QString session =input.mid(sessionStart + type.length(), sessionEnd);
thing->setStateValue(stateTypeId, session);
}
};
extractSession(tmateSshStateTypeId, "ssh session: ssh ", data);
extractSession(tmateSshRoStateTypeId, "ssh session read only: ssh ", data);
extractSession(tmateWebStateTypeId, "web session: ", data);
extractSession(tmateWebRoStateTypeId, "web session read only: ", data);
QRegularExpression joinAddressRegex("joined \\(([0-9\\.]+)\\)");
QRegularExpressionMatchIterator it = joinAddressRegex.globalMatch(data);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString word = match.captured(1);
qCInfo(dcTmate()) << "Connected:" << word;
thing->emitEvent(tmateClientConnectedEventTypeId, {{tmateClientConnectedEventClientAddressParamTypeId, word}});
thing->setStateValue(tmateClientsStateTypeId, thing->stateValue(tmateClientsStateTypeId).toUInt()+1);
}
QRegularExpression leftAddressRegex("left \\(([0-9\\.]+)\\)");
it = leftAddressRegex.globalMatch(data);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString word = match.captured(1);
qCInfo(dcTmate()) << "Disconnected:" << word;
thing->emitEvent(tmateClientDisconnectedEventTypeId, {{tmateClientDisconnectedEventClientAddressParamTypeId, word}});
thing->setStateValue(tmateClientsStateTypeId, thing->stateValue(tmateClientsStateTypeId).toUInt()-1);
}
}
});
// Start up now if enabled
bool enabled = thing->stateValue(tmateActiveStateTypeId).toBool();
if (enabled) {
process->start();
}
info->finish(Thing::ThingErrorNoError);
// Create a watchdog to reconnect if a connection drops...
if (!m_watchdog) {
m_watchdog = hardwareManager()->pluginTimerManager()->registerTimer(10);
connect(m_watchdog, &PluginTimer::timeout, this, [this](){
foreach (Thing *thing, m_processes.keys()) {
QProcess *process = m_processes.value(thing);
if (thing->stateValue(tmateActiveStateTypeId).toBool() && process->state() == QProcess::NotRunning) {
qCInfo(dcTmate()) << "Reconnecting tmate for" << thing->name();
process->start();
}
}
});
}
}
void IntegrationPluginTmate::thingRemoved(Thing *thing)
{
if (thing->thingClassId() == tmateThingClassId) {
QProcess *process = m_processes.take(thing);
if (process->state() != QProcess::NotRunning) {
process->terminate();
process->waitForFinished();
}
}
if (myThings().isEmpty()) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_watchdog);
m_watchdog = nullptr;
}
}
void IntegrationPluginTmate::executeAction(ThingActionInfo *info)
{
if (info->action().actionTypeId() == tmateActiveActionTypeId) {
bool active = info->action().paramValue(tmateActiveActionActiveParamTypeId).toBool();
QProcess *process = m_processes.value(info->thing());
if (active) {
qCInfo(dcTmate()) << "Reconnecting tmate for" << info->thing()->name();
process->start();
} else {
qCInfo(dcTmate()) << "Terminating session for" << info->thing()->name();
process->terminate();
}
info->thing()->setStateValue(tmateActiveStateTypeId, active);
info->finish(Thing::ThingErrorNoError);
}
}