Add threaded session backend architecture with real SSH backend
This commit is contained in:
363
src/session_tab.cpp
Normal file
363
src/session_tab.cpp
Normal file
@@ -0,0 +1,363 @@
|
||||
#include "session_tab.h"
|
||||
|
||||
#include "session_backend_factory.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDateTime>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QInputDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QThread>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <memory>
|
||||
|
||||
SessionTab::SessionTab(const Profile& profile, QWidget* parent)
|
||||
: QWidget(parent),
|
||||
m_profile(profile),
|
||||
m_backendThread(new QThread(this)),
|
||||
m_backend(nullptr),
|
||||
m_state(SessionState::Disconnected),
|
||||
m_statusLabel(nullptr),
|
||||
m_errorLabel(nullptr),
|
||||
m_eventLog(nullptr),
|
||||
m_connectButton(nullptr),
|
||||
m_disconnectButton(nullptr),
|
||||
m_reconnectButton(nullptr),
|
||||
m_copyErrorButton(nullptr)
|
||||
{
|
||||
qRegisterMetaType<SessionConnectOptions>("SessionConnectOptions");
|
||||
qRegisterMetaType<SessionState>("SessionState");
|
||||
|
||||
setupUi();
|
||||
|
||||
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(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);
|
||||
|
||||
m_backendThread->start();
|
||||
|
||||
setState(SessionState::Disconnected, QStringLiteral("Ready to connect."));
|
||||
}
|
||||
|
||||
SessionTab::~SessionTab()
|
||||
{
|
||||
if (m_backend != 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::onConnectClicked()
|
||||
{
|
||||
if (!validateProfileForConnect()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::optional<SessionConnectOptions> options = buildConnectOptions();
|
||||
if (!options.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit requestConnect(options.value());
|
||||
}
|
||||
|
||||
void SessionTab::onDisconnectClicked()
|
||||
{
|
||||
emit requestDisconnect();
|
||||
}
|
||||
|
||||
void SessionTab::onReconnectClicked()
|
||||
{
|
||||
if (!validateProfileForConnect()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::optional<SessionConnectOptions> options = buildConnectOptions();
|
||||
if (!options.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit requestReconnect(options.value());
|
||||
}
|
||||
|
||||
void SessionTab::onCopyErrorClicked()
|
||||
{
|
||||
if (m_lastError.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QGuiApplication::clipboard()->setText(m_lastError);
|
||||
appendEvent(QStringLiteral("Copied last error to clipboard."));
|
||||
}
|
||||
|
||||
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;
|
||||
m_errorLabel->setText(QStringLiteral("Last Error: %1").arg(displayMessage));
|
||||
m_copyErrorButton->setEnabled(true);
|
||||
}
|
||||
|
||||
void SessionTab::setupUi()
|
||||
{
|
||||
auto* rootLayout = new QVBoxLayout(this);
|
||||
|
||||
auto* profileLabel = new QLabel(QStringLiteral("Profile: %1").arg(m_profile.name), this);
|
||||
auto* endpointLabel = new QLabel(
|
||||
QStringLiteral("Endpoint: %1://%2@%3:%4")
|
||||
.arg(m_profile.protocol,
|
||||
m_profile.username.isEmpty() ? QStringLiteral("<none>") : m_profile.username,
|
||||
m_profile.host,
|
||||
QString::number(m_profile.port)),
|
||||
this);
|
||||
auto* authLabel = new QLabel(QStringLiteral("Auth: %1").arg(m_profile.authMode), this);
|
||||
|
||||
m_statusLabel = new QLabel(this);
|
||||
m_errorLabel = new QLabel(QStringLiteral("Last Error: None"), this);
|
||||
m_errorLabel->setWordWrap(true);
|
||||
|
||||
auto* actionRow = new QHBoxLayout();
|
||||
m_connectButton = new QPushButton(QStringLiteral("Connect"), this);
|
||||
m_disconnectButton = new QPushButton(QStringLiteral("Disconnect"), this);
|
||||
m_reconnectButton = new QPushButton(QStringLiteral("Reconnect"), this);
|
||||
m_copyErrorButton = new QPushButton(QStringLiteral("Copy Error"), this);
|
||||
|
||||
actionRow->addWidget(m_connectButton);
|
||||
actionRow->addWidget(m_disconnectButton);
|
||||
actionRow->addWidget(m_reconnectButton);
|
||||
actionRow->addWidget(m_copyErrorButton);
|
||||
actionRow->addStretch();
|
||||
|
||||
auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), this);
|
||||
QFont surfaceFont = surfaceLabel->font();
|
||||
surfaceFont.setPointSize(surfaceFont.pointSize() + 6);
|
||||
surfaceFont.setBold(true);
|
||||
surfaceLabel->setFont(surfaceFont);
|
||||
surfaceLabel->setAlignment(Qt::AlignCenter);
|
||||
surfaceLabel->setMinimumHeight(180);
|
||||
surfaceLabel->setStyleSheet(
|
||||
QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;"));
|
||||
|
||||
m_eventLog = new QPlainTextEdit(this);
|
||||
m_eventLog->setReadOnly(true);
|
||||
m_eventLog->setPlaceholderText(QStringLiteral("Session event log..."));
|
||||
m_eventLog->setMinimumHeight(180);
|
||||
|
||||
rootLayout->addWidget(profileLabel);
|
||||
rootLayout->addWidget(endpointLabel);
|
||||
rootLayout->addWidget(authLabel);
|
||||
rootLayout->addWidget(m_statusLabel);
|
||||
rootLayout->addWidget(m_errorLabel);
|
||||
rootLayout->addLayout(actionRow);
|
||||
rootLayout->addWidget(surfaceLabel);
|
||||
rootLayout->addWidget(m_eventLog, 1);
|
||||
|
||||
connect(m_connectButton, &QPushButton::clicked, this, &SessionTab::onConnectClicked);
|
||||
connect(m_disconnectButton, &QPushButton::clicked, this, &SessionTab::onDisconnectClicked);
|
||||
connect(m_reconnectButton, &QPushButton::clicked, this, &SessionTab::onReconnectClicked);
|
||||
connect(m_copyErrorButton, &QPushButton::clicked, this, &SessionTab::onCopyErrorClicked);
|
||||
}
|
||||
|
||||
std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
|
||||
{
|
||||
SessionConnectOptions options;
|
||||
options.knownHostsPolicy = m_profile.knownHostsPolicy;
|
||||
|
||||
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
|
||||
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.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_profile.host.trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("SSH host is required."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_profile.username.trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("SSH username is required."));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_profile.port < 1 || m_profile.port > 65535) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Connect"),
|
||||
QStringLiteral("SSH port must be between 1 and 65535."));
|
||||
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;
|
||||
|
||||
QString style;
|
||||
switch (state) {
|
||||
case SessionState::Disconnected:
|
||||
style = QStringLiteral("border: 1px solid #8a8a8a; background-color: #efefef; padding: 6px;");
|
||||
break;
|
||||
case SessionState::Connecting:
|
||||
style = QStringLiteral("border: 1px solid #a5a5a5; background-color: #fff3cd; padding: 6px;");
|
||||
break;
|
||||
case SessionState::Connected:
|
||||
style = QStringLiteral("border: 1px solid #3c763d; background-color: #dff0d8; padding: 6px;");
|
||||
break;
|
||||
case SessionState::Failed:
|
||||
style = QStringLiteral("border: 1px solid #a94442; background-color: #f2dede; padding: 6px;");
|
||||
break;
|
||||
}
|
||||
|
||||
m_statusLabel->setStyleSheet(style);
|
||||
m_statusLabel->setText(QStringLiteral("Connection State: %1").arg(message));
|
||||
|
||||
refreshActionButtons();
|
||||
emit tabTitleChanged(tabTitle());
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
m_connectButton->setEnabled(m_state == SessionState::Disconnected
|
||||
|| m_state == SessionState::Failed);
|
||||
m_disconnectButton->setEnabled(m_state == SessionState::Connected
|
||||
|| m_state == SessionState::Connecting);
|
||||
m_reconnectButton->setEnabled(m_state == SessionState::Connected
|
||||
|| m_state == SessionState::Failed
|
||||
|| m_state == SessionState::Disconnected);
|
||||
m_copyErrorButton->setEnabled(!m_lastError.isEmpty());
|
||||
}
|
||||
Reference in New Issue
Block a user