// 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 . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "integrationplugintmate.h" #include "plugininfo.h" #include #include #include #include #include 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); } }