806 lines
27 KiB
C++
806 lines
27 KiB
C++
#include "session_tab.h"
|
|
|
|
#include "rdp_display_widget.h"
|
|
#include "session_backend_factory.h"
|
|
#include "terminal_view.h"
|
|
|
|
#include <KodoTerm/KodoTerm.hpp>
|
|
|
|
#include <QDateTime>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QFont>
|
|
#include <QFontDatabase>
|
|
#include <QHBoxLayout>
|
|
#include <QInputDialog>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QPlainTextEdit>
|
|
#include <QProcessEnvironment>
|
|
#include <QThread>
|
|
#include <QTimer>
|
|
#include <QToolButton>
|
|
#include <QVBoxLayout>
|
|
|
|
#include <memory>
|
|
|
|
namespace {
|
|
QFont defaultTerminalFont()
|
|
{
|
|
QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
|
font.setStyleHint(QFont::Monospace);
|
|
font.setFixedPitch(true);
|
|
font.setKerning(false);
|
|
font.setLetterSpacing(QFont::AbsoluteSpacing, 0.0);
|
|
return font;
|
|
}
|
|
|
|
TerminalTheme themeForName(const QString& themeName)
|
|
{
|
|
if (themeName.compare(QStringLiteral("Light"), Qt::CaseInsensitive) == 0) {
|
|
return TerminalTheme::loadKonsoleTheme(
|
|
QStringLiteral(":/KodoTermThemes/konsole/BlackOnWhite.colorscheme"));
|
|
}
|
|
|
|
if (themeName.compare(QStringLiteral("Solarized Dark"), Qt::CaseInsensitive) == 0) {
|
|
return TerminalTheme::loadKonsoleTheme(
|
|
QStringLiteral(":/KodoTermThemes/konsole/Solarized.colorscheme"));
|
|
}
|
|
|
|
return TerminalTheme::loadKonsoleTheme(
|
|
QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme"));
|
|
}
|
|
}
|
|
|
|
SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
|
: QWidget(parent),
|
|
m_profile(profile),
|
|
m_backendThread(nullptr),
|
|
m_backend(nullptr),
|
|
m_useKodoTermForSsh(profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive)
|
|
== 0),
|
|
m_state(SessionState::Disconnected),
|
|
m_terminalThemeName(QStringLiteral("Dark")),
|
|
m_sshTerminal(nullptr),
|
|
m_rdpDisplay(nullptr),
|
|
m_terminalOutput(nullptr),
|
|
m_eventLog(nullptr),
|
|
m_toggleEventsButton(nullptr),
|
|
m_eventsPanel(nullptr)
|
|
{
|
|
qRegisterMetaType<SessionConnectOptions>("SessionConnectOptions");
|
|
qRegisterMetaType<SessionState>("SessionState");
|
|
|
|
setupUi();
|
|
|
|
if (m_useKodoTermForSsh) {
|
|
connect(m_sshTerminal,
|
|
&KodoTerm::finished,
|
|
this,
|
|
[this](int exitCode, int) {
|
|
if (m_state == SessionState::Disconnected) {
|
|
return;
|
|
}
|
|
|
|
if (m_state == SessionState::Connected) {
|
|
if (exitCode != 0) {
|
|
appendEvent(QStringLiteral("SSH session closed with exit code %1.")
|
|
.arg(exitCode));
|
|
}
|
|
setState(SessionState::Disconnected,
|
|
QStringLiteral("SSH session closed."));
|
|
return;
|
|
}
|
|
|
|
if (exitCode == 0) {
|
|
setState(SessionState::Disconnected,
|
|
QStringLiteral("SSH session ended."));
|
|
return;
|
|
}
|
|
|
|
m_lastError = QStringLiteral("ssh exited with code %1").arg(exitCode);
|
|
appendEvent(QStringLiteral("Error: %1").arg(m_lastError));
|
|
setState(SessionState::Failed,
|
|
QStringLiteral("SSH session exited unexpectedly."));
|
|
});
|
|
connect(m_sshTerminal,
|
|
&KodoTerm::cwdChanged,
|
|
this,
|
|
[this](const QString& cwd) {
|
|
if (!cwd.trimmed().isEmpty()) {
|
|
appendEvent(QStringLiteral("Remote cwd: %1").arg(cwd));
|
|
}
|
|
});
|
|
} else {
|
|
m_backendThread = new QThread(this);
|
|
|
|
std::unique_ptr<SessionBackend> backend = createSessionBackend(m_profile);
|
|
m_backend = backend.release();
|
|
m_backend->moveToThread(m_backendThread);
|
|
|
|
connect(m_backendThread, &QThread::finished, m_backend, &QObject::deleteLater);
|
|
connect(this,
|
|
&SessionTab::requestConnect,
|
|
m_backend,
|
|
&SessionBackend::connectSession,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestDisconnect,
|
|
m_backend,
|
|
&SessionBackend::disconnectSession,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestReconnect,
|
|
m_backend,
|
|
&SessionBackend::reconnectSession,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestInput,
|
|
m_backend,
|
|
&SessionBackend::sendInput,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestHostKeyConfirmation,
|
|
m_backend,
|
|
&SessionBackend::confirmHostKey,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestTerminalSize,
|
|
m_backend,
|
|
&SessionBackend::updateTerminalSize,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestKeyEvent,
|
|
m_backend,
|
|
&SessionBackend::sendKeyEvent,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestMouseMoveEvent,
|
|
m_backend,
|
|
&SessionBackend::sendMouseMoveEvent,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestMouseButtonEvent,
|
|
m_backend,
|
|
&SessionBackend::sendMouseButtonEvent,
|
|
Qt::QueuedConnection);
|
|
connect(this,
|
|
&SessionTab::requestMouseWheelEvent,
|
|
m_backend,
|
|
&SessionBackend::sendMouseWheelEvent,
|
|
Qt::QueuedConnection);
|
|
|
|
connect(m_backend,
|
|
&SessionBackend::stateChanged,
|
|
this,
|
|
&SessionTab::onBackendStateChanged,
|
|
Qt::QueuedConnection);
|
|
connect(m_backend,
|
|
&SessionBackend::eventLogged,
|
|
this,
|
|
&SessionTab::onBackendEventLogged,
|
|
Qt::QueuedConnection);
|
|
connect(m_backend,
|
|
&SessionBackend::connectionError,
|
|
this,
|
|
&SessionTab::onBackendConnectionError,
|
|
Qt::QueuedConnection);
|
|
connect(m_backend,
|
|
&SessionBackend::outputReceived,
|
|
this,
|
|
&SessionTab::onBackendOutputReceived,
|
|
Qt::QueuedConnection);
|
|
connect(m_backend,
|
|
&SessionBackend::hostKeyConfirmationRequested,
|
|
this,
|
|
&SessionTab::onBackendHostKeyConfirmationRequested,
|
|
Qt::QueuedConnection);
|
|
connect(m_backend,
|
|
&SessionBackend::frameUpdated,
|
|
this,
|
|
[this](const QImage& frame) {
|
|
if (m_rdpDisplay != nullptr) {
|
|
m_rdpDisplay->setFrame(frame);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
connect(m_backend,
|
|
&SessionBackend::remoteDesktopSizeChanged,
|
|
this,
|
|
[this](int width, int height) {
|
|
if (m_rdpDisplay != nullptr) {
|
|
m_rdpDisplay->setRemoteDesktopSize(width, height);
|
|
}
|
|
},
|
|
Qt::QueuedConnection);
|
|
|
|
m_backendThread->start();
|
|
}
|
|
|
|
setState(SessionState::Disconnected, QStringLiteral("Ready to connect."));
|
|
QTimer::singleShot(0, this, &SessionTab::connectSession);
|
|
}
|
|
|
|
SessionTab::~SessionTab()
|
|
{
|
|
if (m_useKodoTermForSsh && m_sshTerminal != nullptr && m_state != SessionState::Disconnected) {
|
|
m_sshTerminal->kill();
|
|
}
|
|
|
|
if (m_backend != nullptr && m_backendThread != nullptr && m_backendThread->isRunning()) {
|
|
QMetaObject::invokeMethod(m_backend, "disconnectSession", Qt::BlockingQueuedConnection);
|
|
m_backendThread->quit();
|
|
m_backendThread->wait(2000);
|
|
}
|
|
}
|
|
|
|
QString SessionTab::tabTitle() const
|
|
{
|
|
return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix());
|
|
}
|
|
|
|
void SessionTab::connectSession()
|
|
{
|
|
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
|
|
return;
|
|
}
|
|
|
|
if (!validateProfileForConnect()) {
|
|
return;
|
|
}
|
|
|
|
const std::optional<SessionConnectOptions> options = buildConnectOptions();
|
|
if (!options.has_value()) {
|
|
return;
|
|
}
|
|
|
|
m_lastConnectOptions = options.value();
|
|
|
|
if (m_useKodoTermForSsh) {
|
|
if (!startSshTerminal(options.value())) {
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
emit requestConnect(options.value());
|
|
}
|
|
|
|
void SessionTab::disconnectSession()
|
|
{
|
|
if (m_state == SessionState::Disconnected) {
|
|
return;
|
|
}
|
|
|
|
if (m_useKodoTermForSsh) {
|
|
if (m_sshTerminal != nullptr) {
|
|
m_sshTerminal->kill();
|
|
}
|
|
setState(SessionState::Disconnected, QStringLiteral("Session disconnected."));
|
|
return;
|
|
}
|
|
|
|
emit requestDisconnect();
|
|
}
|
|
|
|
void SessionTab::reconnectSession()
|
|
{
|
|
if (!validateProfileForConnect()) {
|
|
return;
|
|
}
|
|
|
|
const std::optional<SessionConnectOptions> options = buildConnectOptions();
|
|
if (!options.has_value()) {
|
|
return;
|
|
}
|
|
|
|
m_lastConnectOptions = options.value();
|
|
|
|
if (m_useKodoTermForSsh) {
|
|
if (m_sshTerminal != nullptr) {
|
|
m_sshTerminal->kill();
|
|
}
|
|
QTimer::singleShot(50,
|
|
this,
|
|
[this, options]() { startSshTerminal(options.value()); });
|
|
return;
|
|
}
|
|
|
|
emit requestReconnect(options.value());
|
|
}
|
|
|
|
void SessionTab::clearTerminal()
|
|
{
|
|
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
|
|
m_sshTerminal->clearScrollback();
|
|
m_sshTerminal->setFocus();
|
|
return;
|
|
}
|
|
|
|
if (m_terminalOutput != nullptr) {
|
|
m_terminalOutput->clear();
|
|
if (m_state == SessionState::Connected) {
|
|
emit requestInput(QStringLiteral("\x0c"));
|
|
}
|
|
m_terminalOutput->setFocus();
|
|
return;
|
|
}
|
|
|
|
if (m_rdpDisplay != nullptr) {
|
|
m_rdpDisplay->clearFrame();
|
|
m_rdpDisplay->setFocus();
|
|
}
|
|
}
|
|
|
|
void SessionTab::setTerminalThemeName(const QString& themeName)
|
|
{
|
|
const QString normalized = themeName.trimmed();
|
|
if (normalized.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (m_terminalThemeName.compare(normalized, Qt::CaseInsensitive) == 0) {
|
|
return;
|
|
}
|
|
|
|
m_terminalThemeName = normalized;
|
|
applyTerminalTheme(m_terminalThemeName);
|
|
appendEvent(QStringLiteral("Terminal theme set to %1.").arg(m_terminalThemeName));
|
|
}
|
|
|
|
QString SessionTab::terminalThemeName() const
|
|
{
|
|
return m_terminalThemeName;
|
|
}
|
|
|
|
bool SessionTab::supportsThemeSelection() const
|
|
{
|
|
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
|
|
}
|
|
|
|
bool SessionTab::supportsClearAction() const
|
|
{
|
|
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
|
|
}
|
|
|
|
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
|
|
{
|
|
setState(state, message);
|
|
}
|
|
|
|
void SessionTab::onBackendEventLogged(const QString& message)
|
|
{
|
|
appendEvent(message);
|
|
}
|
|
|
|
void SessionTab::onBackendConnectionError(const QString& displayMessage, const QString& rawMessage)
|
|
{
|
|
m_lastError = rawMessage.isEmpty() ? displayMessage : rawMessage;
|
|
appendEvent(QStringLiteral("Error: %1").arg(displayMessage));
|
|
if (!rawMessage.trimmed().isEmpty() && rawMessage.trimmed() != displayMessage.trimmed()) {
|
|
appendEvent(QStringLiteral("Raw Error: %1").arg(rawMessage.trimmed()));
|
|
}
|
|
}
|
|
|
|
void SessionTab::onBackendOutputReceived(const QString& text)
|
|
{
|
|
if (text.isEmpty() || m_terminalOutput == nullptr) {
|
|
return;
|
|
}
|
|
|
|
m_terminalOutput->appendTerminalData(text);
|
|
}
|
|
|
|
void SessionTab::onBackendHostKeyConfirmationRequested(const QString& prompt)
|
|
{
|
|
const QString question = prompt.isEmpty()
|
|
? QStringLiteral("Unknown SSH host key. Do you trust this host?")
|
|
: prompt;
|
|
|
|
const QMessageBox::StandardButton reply = QMessageBox::question(
|
|
this,
|
|
QStringLiteral("SSH Host Key Confirmation"),
|
|
QStringLiteral("%1\n\nTrust and continue?").arg(question),
|
|
QMessageBox::Yes | QMessageBox::No,
|
|
QMessageBox::No);
|
|
|
|
emit requestHostKeyConfirmation(reply == QMessageBox::Yes);
|
|
}
|
|
|
|
void SessionTab::setupUi()
|
|
{
|
|
auto* rootLayout = new QVBoxLayout(this);
|
|
|
|
if (m_useKodoTermForSsh) {
|
|
m_sshTerminal = new KodoTerm(this);
|
|
const QFont terminalFont = defaultTerminalFont();
|
|
|
|
KodoTermConfig config = m_sshTerminal->getConfig();
|
|
config.font = terminalFont;
|
|
config.textAntialiasing = true;
|
|
config.maxScrollback = 12000;
|
|
m_sshTerminal->setConfig(config);
|
|
rootLayout->addWidget(m_sshTerminal, 1);
|
|
} else if (m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
|
m_rdpDisplay = new RdpDisplayWidget(this);
|
|
rootLayout->addWidget(m_rdpDisplay, 1);
|
|
} else {
|
|
m_terminalOutput = new TerminalView(this);
|
|
m_terminalOutput->setFont(defaultTerminalFont());
|
|
m_terminalOutput->setMinimumHeight(260);
|
|
m_terminalOutput->setReadOnly(true);
|
|
if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
|
m_terminalOutput->setPlaceholderText(
|
|
QStringLiteral("Embedded VNC session output appears here when the backend is available."));
|
|
} else {
|
|
m_terminalOutput->setPlaceholderText(
|
|
QStringLiteral("Session output appears here."));
|
|
}
|
|
rootLayout->addWidget(m_terminalOutput, 1);
|
|
}
|
|
|
|
applyTerminalTheme(m_terminalThemeName);
|
|
|
|
auto* eventsHeader = new QHBoxLayout();
|
|
m_toggleEventsButton = new QToolButton(this);
|
|
m_toggleEventsButton->setCheckable(true);
|
|
eventsHeader->addWidget(m_toggleEventsButton);
|
|
eventsHeader->addStretch();
|
|
|
|
m_eventsPanel = new QWidget(this);
|
|
auto* eventsLayout = new QVBoxLayout(m_eventsPanel);
|
|
eventsLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
auto* eventTitle = new QLabel(QStringLiteral("Session Events"), m_eventsPanel);
|
|
m_eventLog = new QPlainTextEdit(m_eventsPanel);
|
|
m_eventLog->setReadOnly(true);
|
|
m_eventLog->setPlaceholderText(QStringLiteral("Session event log..."));
|
|
m_eventLog->setMinimumHeight(140);
|
|
|
|
eventsLayout->addWidget(eventTitle);
|
|
eventsLayout->addWidget(m_eventLog);
|
|
|
|
rootLayout->addLayout(eventsHeader);
|
|
rootLayout->addWidget(m_eventsPanel);
|
|
|
|
setPanelExpanded(m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), false);
|
|
|
|
connect(m_toggleEventsButton,
|
|
&QToolButton::toggled,
|
|
this,
|
|
[this](bool expanded) {
|
|
setPanelExpanded(
|
|
m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), expanded);
|
|
});
|
|
|
|
if (m_terminalOutput != nullptr) {
|
|
connect(m_terminalOutput,
|
|
&TerminalView::inputGenerated,
|
|
this,
|
|
[this](const QString& input) { emit requestInput(input); });
|
|
connect(m_terminalOutput,
|
|
&TerminalView::terminalSizeChanged,
|
|
this,
|
|
[this](int columns, int rows) { emit requestTerminalSize(columns, rows); });
|
|
} else if (m_rdpDisplay != nullptr) {
|
|
connect(m_rdpDisplay,
|
|
&RdpDisplayWidget::viewportSizeChanged,
|
|
this,
|
|
[this](int width, int height) { emit requestTerminalSize(width, height); });
|
|
connect(m_rdpDisplay,
|
|
&RdpDisplayWidget::keyInput,
|
|
this,
|
|
[this](int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers) {
|
|
emit requestKeyEvent(key, nativeScanCode, text, pressed, modifiers);
|
|
});
|
|
connect(m_rdpDisplay,
|
|
&RdpDisplayWidget::mouseMoveInput,
|
|
this,
|
|
[this](int x, int y) { emit requestMouseMoveEvent(x, y); });
|
|
connect(m_rdpDisplay,
|
|
&RdpDisplayWidget::mouseButtonInput,
|
|
this,
|
|
[this](int x, int y, int button, bool pressed) {
|
|
emit requestMouseButtonEvent(x, y, button, pressed);
|
|
});
|
|
connect(m_rdpDisplay,
|
|
&RdpDisplayWidget::mouseWheelInput,
|
|
this,
|
|
[this](int x, int y, int deltaX, int deltaY) {
|
|
emit requestMouseWheelEvent(x, y, deltaX, deltaY);
|
|
});
|
|
}
|
|
}
|
|
|
|
std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
|
|
{
|
|
SessionConnectOptions options;
|
|
options.knownHostsPolicy = m_profile.knownHostsPolicy;
|
|
|
|
const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0;
|
|
const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0;
|
|
|
|
if (!isSsh && !isRdp) {
|
|
return options;
|
|
}
|
|
|
|
if (isRdp) {
|
|
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) != 0) {
|
|
return options;
|
|
}
|
|
|
|
bool accepted = false;
|
|
const QString password = QInputDialog::getText(
|
|
this,
|
|
QStringLiteral("RDP Password"),
|
|
QStringLiteral("Password for %1:")
|
|
.arg(m_profile.username.trimmed().isEmpty()
|
|
? m_profile.host
|
|
: QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host)),
|
|
QLineEdit::Password,
|
|
QString(),
|
|
&accepted);
|
|
if (!accepted) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (password.isEmpty()) {
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Connect"),
|
|
QStringLiteral("Password is required for password authentication."));
|
|
return std::nullopt;
|
|
}
|
|
|
|
options.password = password;
|
|
return options;
|
|
}
|
|
|
|
if (m_useKodoTermForSsh
|
|
&& m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
|
|
// Password is entered directly in terminal prompt.
|
|
return options;
|
|
}
|
|
|
|
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
|
|
bool accepted = false;
|
|
const QString password = QInputDialog::getText(this,
|
|
QStringLiteral("SSH Password"),
|
|
QStringLiteral("Password for %1@%2:")
|
|
.arg(m_profile.username, m_profile.host),
|
|
QLineEdit::Password,
|
|
QString(),
|
|
&accepted);
|
|
if (!accepted) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (password.isEmpty()) {
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Connect"),
|
|
QStringLiteral("Password is required for password authentication."));
|
|
return std::nullopt;
|
|
}
|
|
|
|
options.password = password;
|
|
return options;
|
|
}
|
|
|
|
QString keyPath = m_profile.privateKeyPath.trimmed();
|
|
if (keyPath.isEmpty()) {
|
|
keyPath = QFileDialog::getOpenFileName(this,
|
|
QStringLiteral("Select Private Key"),
|
|
QString(),
|
|
QStringLiteral("All Files (*)"));
|
|
if (keyPath.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
if (!QFileInfo::exists(keyPath)) {
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Connect"),
|
|
QStringLiteral("Private key file not found: %1").arg(keyPath));
|
|
return std::nullopt;
|
|
}
|
|
|
|
options.privateKeyPath = keyPath;
|
|
return options;
|
|
}
|
|
|
|
bool SessionTab::validateProfileForConnect()
|
|
{
|
|
if (m_profile.host.trimmed().isEmpty()) {
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Connect"),
|
|
QStringLiteral("%1 host is required.").arg(m_profile.protocol));
|
|
return false;
|
|
}
|
|
|
|
if (m_profile.port < 1 || m_profile.port > 65535) {
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Connect"),
|
|
QStringLiteral("Port must be between 1 and 65535."));
|
|
return false;
|
|
}
|
|
|
|
if ((m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0
|
|
|| m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0)
|
|
&& m_profile.username.trimmed().isEmpty()) {
|
|
QMessageBox::warning(this,
|
|
QStringLiteral("Connect"),
|
|
QStringLiteral("%1 username is required.").arg(m_profile.protocol));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SessionTab::appendEvent(const QString& message)
|
|
{
|
|
const QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
|
m_eventLog->appendPlainText(QStringLiteral("[%1] %2").arg(timestamp, message));
|
|
}
|
|
|
|
void SessionTab::setState(SessionState state, const QString& message)
|
|
{
|
|
m_state = state;
|
|
appendEvent(QStringLiteral("Connection state: %1").arg(message));
|
|
|
|
refreshActionButtons();
|
|
emit tabTitleChanged(tabTitle());
|
|
emit tabStateChanged(state);
|
|
}
|
|
|
|
QString SessionTab::stateSuffix() const
|
|
{
|
|
switch (m_state) {
|
|
case SessionState::Disconnected:
|
|
return QStringLiteral("Disconnected");
|
|
case SessionState::Connecting:
|
|
return QStringLiteral("Connecting");
|
|
case SessionState::Connected:
|
|
return QStringLiteral("Connected");
|
|
case SessionState::Failed:
|
|
return QStringLiteral("Failed");
|
|
}
|
|
|
|
return QStringLiteral("Unknown");
|
|
}
|
|
|
|
void SessionTab::refreshActionButtons()
|
|
{
|
|
const bool isConnected = m_state == SessionState::Connected;
|
|
|
|
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
|
|
m_sshTerminal->setEnabled(true);
|
|
m_sshTerminal->setFocus();
|
|
return;
|
|
}
|
|
|
|
if (m_terminalOutput != nullptr) {
|
|
m_terminalOutput->setEnabled(isConnected);
|
|
m_terminalOutput->setFocus();
|
|
return;
|
|
}
|
|
|
|
if (m_rdpDisplay != nullptr) {
|
|
m_rdpDisplay->setEnabled(isConnected);
|
|
if (isConnected) {
|
|
m_rdpDisplay->setFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SessionTab::setPanelExpanded(QToolButton* button,
|
|
QWidget* panel,
|
|
const QString& name,
|
|
bool expanded)
|
|
{
|
|
if (button == nullptr || panel == nullptr) {
|
|
return;
|
|
}
|
|
|
|
button->blockSignals(true);
|
|
button->setChecked(expanded);
|
|
button->blockSignals(false);
|
|
|
|
panel->setVisible(expanded);
|
|
button->setText(expanded ? QStringLiteral("Hide %1").arg(name)
|
|
: QStringLiteral("Show %1").arg(name));
|
|
}
|
|
|
|
bool SessionTab::startSshTerminal(const SessionConnectOptions& options)
|
|
{
|
|
if (m_sshTerminal == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
QStringList args;
|
|
args << QStringLiteral("-tt") << QStringLiteral("-p") << QString::number(m_profile.port)
|
|
<< QStringLiteral("-o") << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o")
|
|
<< QStringLiteral("ServerAliveInterval=20") << QStringLiteral("-o")
|
|
<< QStringLiteral("ServerAliveCountMax=2");
|
|
|
|
const QString policy = options.knownHostsPolicy.trimmed().isEmpty()
|
|
? m_profile.knownHostsPolicy.trimmed()
|
|
: options.knownHostsPolicy.trimmed();
|
|
|
|
if (policy.compare(QStringLiteral("Ignore"), Qt::CaseInsensitive) == 0) {
|
|
#ifdef Q_OS_WIN
|
|
const QString knownHostsNullDevice = QStringLiteral("NUL");
|
|
#else
|
|
const QString knownHostsNullDevice = QStringLiteral("/dev/null");
|
|
#endif
|
|
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=no")
|
|
<< QStringLiteral("-o")
|
|
<< QStringLiteral("UserKnownHostsFile=%1").arg(knownHostsNullDevice);
|
|
} else if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
|
|
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=accept-new");
|
|
} else if (policy.compare(QStringLiteral("Ask"), Qt::CaseInsensitive) == 0) {
|
|
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=ask");
|
|
} else {
|
|
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes");
|
|
}
|
|
|
|
if (m_profile.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
|
|
QString keyPath = options.privateKeyPath.trimmed();
|
|
if (keyPath.isEmpty()) {
|
|
keyPath = m_profile.privateKeyPath.trimmed();
|
|
}
|
|
|
|
if (keyPath.isEmpty()) {
|
|
m_lastError = QStringLiteral("Private key path is required.");
|
|
appendEvent(QStringLiteral("Error: %1").arg(m_lastError));
|
|
setState(SessionState::Failed, m_lastError);
|
|
return false;
|
|
}
|
|
|
|
args << QStringLiteral("-i") << keyPath;
|
|
}
|
|
|
|
const QString target = m_profile.username.trimmed().isEmpty()
|
|
? m_profile.host.trimmed()
|
|
: QStringLiteral("%1@%2").arg(m_profile.username.trimmed(), m_profile.host.trimmed());
|
|
args << target;
|
|
|
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
|
if (!env.contains(QStringLiteral("TERM"))) {
|
|
env.insert(QStringLiteral("TERM"), QStringLiteral("xterm-256color"));
|
|
}
|
|
if (!env.contains(QStringLiteral("COLORTERM"))) {
|
|
env.insert(QStringLiteral("COLORTERM"), QStringLiteral("truecolor"));
|
|
}
|
|
|
|
m_sshTerminal->setProgram(QStringLiteral("ssh"));
|
|
m_sshTerminal->setArguments(args);
|
|
m_sshTerminal->setProcessEnvironment(env);
|
|
|
|
appendEvent(QStringLiteral("Launching SSH terminal session."));
|
|
setState(SessionState::Connecting, QStringLiteral("Starting SSH terminal..."));
|
|
|
|
if (!m_sshTerminal->start()) {
|
|
m_lastError = QStringLiteral("Failed to start embedded SSH terminal process.");
|
|
appendEvent(QStringLiteral("Error: %1").arg(m_lastError));
|
|
setState(SessionState::Failed, QStringLiteral("Failed to start SSH terminal."));
|
|
return false;
|
|
}
|
|
|
|
setState(SessionState::Connected, QStringLiteral("SSH session established."));
|
|
return true;
|
|
}
|
|
|
|
void SessionTab::applyTerminalTheme(const QString& themeName)
|
|
{
|
|
if (m_useKodoTermForSsh) {
|
|
if (m_sshTerminal != nullptr) {
|
|
m_sshTerminal->setTheme(themeForName(themeName));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (m_terminalOutput != nullptr) {
|
|
m_terminalOutput->setThemeName(themeName);
|
|
}
|
|
}
|