192 lines
7.3 KiB
C++
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);
|
|
}
|
|
}
|