Milestone 5: deliver embedded RDP sessions and lifecycle hardening

This commit is contained in:
Keith Smith
2026-03-03 18:59:26 -07:00
parent 230a401386
commit 36006bd4aa
2941 changed files with 724359 additions and 77 deletions

View File

@@ -1,5 +1,6 @@
#include "session_tab.h"
#include "rdp_display_widget.h"
#include "session_backend_factory.h"
#include "terminal_view.h"
@@ -62,6 +63,7 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
m_state(SessionState::Disconnected),
m_terminalThemeName(QStringLiteral("Dark")),
m_sshTerminal(nullptr),
m_rdpDisplay(nullptr),
m_terminalOutput(nullptr),
m_eventLog(nullptr),
m_toggleEventsButton(nullptr),
@@ -148,6 +150,26 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
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,
@@ -174,6 +196,24 @@ SessionTab::SessionTab(const Profile& profile, QWidget* parent)
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();
}
@@ -284,6 +324,12 @@ void SessionTab::clearTerminal()
emit requestInput(QStringLiteral("\x0c"));
}
m_terminalOutput->setFocus();
return;
}
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->clearFrame();
m_rdpDisplay->setFocus();
}
}
@@ -308,6 +354,16 @@ 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);
@@ -322,6 +378,9 @@ void SessionTab::onBackendConnectionError(const QString& displayMessage, const Q
{
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)
@@ -363,12 +422,21 @@ void SessionTab::setupUi()
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->setPlaceholderText(
QStringLiteral("Session is connecting. Type directly here once connected."));
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);
}
@@ -415,6 +483,33 @@ void SessionTab::setupUi()
&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);
});
}
}
@@ -423,7 +518,41 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
SessionConnectOptions options;
options.knownHostsPolicy = m_profile.knownHostsPolicy;
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
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;
}
@@ -481,28 +610,26 @@ std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
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."));
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("SSH port must be between 1 and 65535."));
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;
}
@@ -554,6 +681,14 @@ void SessionTab::refreshActionButtons()
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();
}
}
}