Initial implementation of KRDC SSH plugin
Adds an SSH terminal plugin for KRDC that embeds KonsolePart (Konsole's KPart terminal emulator) inside a RemoteView subclass. The plugin registers the ssh:// URL scheme and launches ssh via the TerminalInterface::startProgram() API. Components: - SshViewFactory: plugin factory handling ssh:// URLs - SshView: RemoteView subclass embedding KonsolePart - SshHostPreferences: configuration for username, identity file, X11/agent forwarding, compression, and port forwarding tunnels Key implementation details: - KonsolePart loaded via KPluginFactory from kf6/parts/konsolepart - ssh binary resolved via QStandardPaths::findExecutable() - scaleResize() unconditionally calls resize() (KRDC's scroll area does not use setWidgetResizable, relies on this slot) - deleteLater() for KonsolePart teardown to avoid use-after-free Build: standalone CMake project linking against krdccore and KF6::Parts.
This commit is contained in:
commit
ac239c1943
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(krdc-ssh VERSION 0.1.0)
|
||||
|
||||
set(QT_MIN_VERSION "6.6.0")
|
||||
set(KF_MIN_VERSION "6.0.0")
|
||||
|
||||
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
|
||||
include(KDEInstallDirs)
|
||||
include(KDECMakeSettings)
|
||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
include(ECMQtDeclareLoggingCategory)
|
||||
include(FeatureSummary)
|
||||
|
||||
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Gui Widgets)
|
||||
|
||||
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
|
||||
Config
|
||||
CoreAddons
|
||||
I18n
|
||||
KIO
|
||||
Parts
|
||||
WidgetsAddons
|
||||
)
|
||||
|
||||
# Find krdccore headers and library
|
||||
find_path(KRDCCORE_INCLUDE_DIR remoteviewfactory.h
|
||||
PATH_SUFFIXES krdc
|
||||
)
|
||||
find_library(KRDCCORE_LIBRARY NAMES krdccore)
|
||||
|
||||
if(NOT KRDCCORE_INCLUDE_DIR OR NOT KRDCCORE_LIBRARY)
|
||||
message(FATAL_ERROR "krdccore not found. Please install KRDC development files.")
|
||||
endif()
|
||||
|
||||
message(STATUS "Found krdccore: ${KRDCCORE_LIBRARY}")
|
||||
message(STATUS "Found krdccore headers: ${KRDCCORE_INCLUDE_DIR}")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
add_definitions(
|
||||
-DQT_USE_QSTRINGBUILDER
|
||||
-DQT_NO_CAST_TO_ASCII
|
||||
-DQT_NO_CAST_FROM_ASCII
|
||||
-DQT_STRICT_ITERATORS
|
||||
-DQT_NO_URL_CAST_FROM_STRING
|
||||
-DQT_NO_CAST_FROM_BYTEARRAY
|
||||
-DQT_NO_KEYWORDS
|
||||
-DQT_NO_FOREACH
|
||||
)
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# KRDC SSH Plugin
|
||||
|
||||
## Architecture
|
||||
|
||||
KRDC plugin that provides SSH terminal sessions. Embeds KonsolePart (KDE's terminal
|
||||
KPart) inside a RemoteView subclass. KonsolePart runs `ssh user@host -p port` via
|
||||
startProgram() — handles terminal emulation, rendering, and input natively.
|
||||
|
||||
## Key Discovery
|
||||
|
||||
KRDC's RemoteViewScrollArea does NOT use setWidgetResizable(true). Instead it emits
|
||||
`resized(w, h)` connected to `scaleResize()`. The view MUST call `resize(w, h)` on
|
||||
itself in scaleResize to get a non-zero size. The `m_connected` guard in resizeEvent
|
||||
prevents the re-entrant crash during tab insertion show sequence.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] Core plugin implementation
|
||||
- [x] Configuration UI (username, identity, tunneling, options)
|
||||
- [x] Fix crash: scaleResize during show (m_connected guard in resizeEvent)
|
||||
- [x] Fix crash: disconnect (deleteLater instead of delete)
|
||||
- [x] Fix blue screen: startProgram needs full path (QStandardPaths)
|
||||
- [x] Fix blue screen: scaleResize isVisible() guard was blocking initial resize
|
||||
- [x] Reverted to startProgram (not showShellInDir) for proper lifecycle
|
||||
- [ ] Verify: no crash, no blue screen, ssh works on first connection
|
||||
- [ ] Clean up dead code / unnecessary deferred resize in resizeEvent
|
||||
- [ ] Squash/tidy commits if desired
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
add_library(krdc_sshplugin MODULE)
|
||||
|
||||
target_sources(krdc_sshplugin PRIVATE
|
||||
sshview.cpp
|
||||
sshviewfactory.cpp
|
||||
sshhostpreferences.cpp
|
||||
)
|
||||
|
||||
ki18n_wrap_ui(krdc_sshplugin
|
||||
sshpreferences.ui
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(krdc_sshplugin
|
||||
HEADER krdc_ssh_debug.h
|
||||
IDENTIFIER KRDC_SSH
|
||||
CATEGORY_NAME KRDC.SSH
|
||||
)
|
||||
|
||||
target_include_directories(krdc_sshplugin PRIVATE
|
||||
${KRDCCORE_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(krdc_sshplugin
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Widgets
|
||||
KF6::ConfigCore
|
||||
KF6::CoreAddons
|
||||
KF6::I18n
|
||||
KF6::KIOWidgets
|
||||
KF6::Parts
|
||||
KF6::WidgetsAddons
|
||||
${KRDCCORE_LIBRARY}
|
||||
)
|
||||
|
||||
install(TARGETS krdc_sshplugin DESTINATION ${KDE_INSTALL_PLUGINDIR}/krdc)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "KRDC SSH Plugin Authors"
|
||||
}
|
||||
],
|
||||
"Category": "Service",
|
||||
"Description": "Allows managing SSH terminal sessions through KRDC",
|
||||
"EnabledByDefault": true,
|
||||
"Icon": "utilities-terminal",
|
||||
"License": "GPL",
|
||||
"Name": "SSH"
|
||||
},
|
||||
"X-KDE-KRDC-Sorting": 30
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "sshhostpreferences.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
static const char username_config_key[] = "username";
|
||||
static const char identity_file_config_key[] = "identityFile";
|
||||
static const char x11_forwarding_config_key[] = "x11Forwarding";
|
||||
static const char agent_forwarding_config_key[] = "agentForwarding";
|
||||
static const char compression_config_key[] = "compression";
|
||||
static const char tunnels_config_key[] = "tunnels";
|
||||
|
||||
SshHostPreferences::SshHostPreferences(KConfigGroup configGroup, QObject *parent)
|
||||
: HostPreferences(configGroup, parent)
|
||||
{
|
||||
}
|
||||
|
||||
SshHostPreferences::~SshHostPreferences()
|
||||
{
|
||||
}
|
||||
|
||||
QWidget *SshHostPreferences::createProtocolSpecificConfigPage(QWidget *sshTunnelWidget)
|
||||
{
|
||||
Q_UNUSED(sshTunnelWidget);
|
||||
|
||||
auto *page = new QWidget();
|
||||
sshUi.setupUi(page);
|
||||
|
||||
// Populate from config
|
||||
sshUi.usernameEdit->setText(userName());
|
||||
sshUi.identityFileEdit->setText(identityFile());
|
||||
sshUi.x11ForwardingCheck->setChecked(x11Forwarding());
|
||||
sshUi.agentForwardingCheck->setChecked(agentForwarding());
|
||||
sshUi.compressionCheck->setChecked(compression());
|
||||
|
||||
// Populate tunnels list
|
||||
const QStringList existingTunnels = tunnels();
|
||||
for (const QString &tunnel : existingTunnels) {
|
||||
sshUi.tunnelList->addItem(tunnel);
|
||||
}
|
||||
|
||||
connect(sshUi.addTunnelButton, &QPushButton::clicked, this, &SshHostPreferences::addTunnel);
|
||||
connect(sshUi.removeTunnelButton, &QPushButton::clicked, this, &SshHostPreferences::removeTunnel);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
void SshHostPreferences::acceptConfig()
|
||||
{
|
||||
HostPreferences::acceptConfig();
|
||||
|
||||
setUserName(sshUi.usernameEdit->text().trimmed());
|
||||
setIdentityFile(sshUi.identityFileEdit->text().trimmed());
|
||||
setX11Forwarding(sshUi.x11ForwardingCheck->isChecked());
|
||||
setAgentForwarding(sshUi.agentForwardingCheck->isChecked());
|
||||
setCompression(sshUi.compressionCheck->isChecked());
|
||||
|
||||
// Collect tunnels from list widget
|
||||
QStringList tunnelList;
|
||||
for (int i = 0; i < sshUi.tunnelList->count(); ++i) {
|
||||
tunnelList << sshUi.tunnelList->item(i)->text();
|
||||
}
|
||||
setTunnels(tunnelList);
|
||||
}
|
||||
|
||||
void SshHostPreferences::addTunnel()
|
||||
{
|
||||
const QString spec = sshUi.tunnelSpecEdit->text().trimmed();
|
||||
if (spec.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString flag;
|
||||
switch (sshUi.tunnelTypeCombo->currentIndex()) {
|
||||
case 0:
|
||||
flag = QStringLiteral("-L");
|
||||
break;
|
||||
case 1:
|
||||
flag = QStringLiteral("-R");
|
||||
break;
|
||||
case 2:
|
||||
flag = QStringLiteral("-D");
|
||||
break;
|
||||
}
|
||||
|
||||
const QString entry = flag + QLatin1Char(' ') + spec;
|
||||
sshUi.tunnelList->addItem(entry);
|
||||
sshUi.tunnelSpecEdit->clear();
|
||||
}
|
||||
|
||||
void SshHostPreferences::removeTunnel()
|
||||
{
|
||||
const auto selected = sshUi.tunnelList->selectedItems();
|
||||
for (auto *item : selected) {
|
||||
delete item;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Config accessors ---
|
||||
|
||||
QString SshHostPreferences::userName() const
|
||||
{
|
||||
return m_configGroup.readEntry(username_config_key, QString());
|
||||
}
|
||||
|
||||
void SshHostPreferences::setUserName(const QString &userName)
|
||||
{
|
||||
m_configGroup.writeEntry(username_config_key, userName);
|
||||
}
|
||||
|
||||
QString SshHostPreferences::identityFile() const
|
||||
{
|
||||
return m_configGroup.readEntry(identity_file_config_key, QString());
|
||||
}
|
||||
|
||||
void SshHostPreferences::setIdentityFile(const QString &path)
|
||||
{
|
||||
m_configGroup.writeEntry(identity_file_config_key, path);
|
||||
}
|
||||
|
||||
bool SshHostPreferences::x11Forwarding() const
|
||||
{
|
||||
return m_configGroup.readEntry(x11_forwarding_config_key, false);
|
||||
}
|
||||
|
||||
void SshHostPreferences::setX11Forwarding(bool enabled)
|
||||
{
|
||||
m_configGroup.writeEntry(x11_forwarding_config_key, enabled);
|
||||
}
|
||||
|
||||
bool SshHostPreferences::agentForwarding() const
|
||||
{
|
||||
return m_configGroup.readEntry(agent_forwarding_config_key, false);
|
||||
}
|
||||
|
||||
void SshHostPreferences::setAgentForwarding(bool enabled)
|
||||
{
|
||||
m_configGroup.writeEntry(agent_forwarding_config_key, enabled);
|
||||
}
|
||||
|
||||
bool SshHostPreferences::compression() const
|
||||
{
|
||||
return m_configGroup.readEntry(compression_config_key, false);
|
||||
}
|
||||
|
||||
void SshHostPreferences::setCompression(bool enabled)
|
||||
{
|
||||
m_configGroup.writeEntry(compression_config_key, enabled);
|
||||
}
|
||||
|
||||
QStringList SshHostPreferences::tunnels() const
|
||||
{
|
||||
return m_configGroup.readEntry(tunnels_config_key, QStringList());
|
||||
}
|
||||
|
||||
void SshHostPreferences::setTunnels(const QStringList &tunnels)
|
||||
{
|
||||
m_configGroup.writeEntry(tunnels_config_key, tunnels);
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef SSHHOSTPREFERENCES_H
|
||||
#define SSHHOSTPREFERENCES_H
|
||||
|
||||
#include "hostpreferences.h"
|
||||
#include "ui_sshpreferences.h"
|
||||
|
||||
class SshHostPreferences : public HostPreferences
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SshHostPreferences(KConfigGroup configGroup, QObject *parent = nullptr);
|
||||
~SshHostPreferences() override;
|
||||
|
||||
QString userName() const;
|
||||
void setUserName(const QString &userName);
|
||||
|
||||
QString identityFile() const;
|
||||
void setIdentityFile(const QString &path);
|
||||
|
||||
bool x11Forwarding() const;
|
||||
void setX11Forwarding(bool enabled);
|
||||
|
||||
bool agentForwarding() const;
|
||||
void setAgentForwarding(bool enabled);
|
||||
|
||||
bool compression() const;
|
||||
void setCompression(bool enabled);
|
||||
|
||||
QStringList tunnels() const;
|
||||
void setTunnels(const QStringList &tunnels);
|
||||
|
||||
protected:
|
||||
QWidget *createProtocolSpecificConfigPage(QWidget *sshTunnelWidget) override;
|
||||
void acceptConfig() override;
|
||||
|
||||
private Q_SLOTS:
|
||||
void addTunnel();
|
||||
void removeTunnel();
|
||||
|
||||
private:
|
||||
Ui::SshPreferences sshUi;
|
||||
};
|
||||
|
||||
#endif // SSHHOSTPREFERENCES_H
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SshPreferences</class>
|
||||
<widget class="QWidget" name="SshPreferences">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>480</width>
|
||||
<height>500</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="connectionGroupBox">
|
||||
<property name="title">
|
||||
<string>Connection</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="connectionFormLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="usernameLabel">
|
||||
<property name="text">
|
||||
<string>&Username:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>usernameEdit</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="usernameEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Leave empty to use current user</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="identityFileLabel">
|
||||
<property name="text">
|
||||
<string>&Identity file:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>identityFileEdit</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="KUrlRequester" name="identityFileEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Default (~/.ssh/id_*)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="optionsGroupBox">
|
||||
<property name="title">
|
||||
<string>Options</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="optionsLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="x11ForwardingCheck">
|
||||
<property name="text">
|
||||
<string>Enable &X11 forwarding (-X)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="agentForwardingCheck">
|
||||
<property name="text">
|
||||
<string>Enable &agent forwarding (-A)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="compressionCheck">
|
||||
<property name="text">
|
||||
<string>Enable &compression (-C)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="tunnelingGroupBox">
|
||||
<property name="title">
|
||||
<string>Port Forwarding</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="tunnelingLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="tunnelTypeLayout">
|
||||
<item>
|
||||
<widget class="QComboBox" name="tunnelTypeCombo">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Local (-L)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Remote (-R)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Dynamic/SOCKS (-D)</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="tunnelSpecEdit">
|
||||
<property name="placeholderText">
|
||||
<string>e.g. 8080:localhost:80</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addTunnelButton">
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="tunnelList">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>120</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeTunnelButton">
|
||||
<property name="text">
|
||||
<string>Remove Selected</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>KUrlRequester</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>kurlrequester.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "sshview.h"
|
||||
#include "krdc_ssh_debug.h"
|
||||
#include "sshhostpreferences.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMimeData>
|
||||
#include <QPainter>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <KPluginFactory>
|
||||
#include <KPluginMetaData>
|
||||
#include <kde_terminal_interface.h>
|
||||
|
||||
SshView::SshView(QWidget *parent, const QUrl &url, KConfigGroup configGroup)
|
||||
: RemoteView(parent)
|
||||
, m_framebufferSize(800, 600)
|
||||
{
|
||||
m_url = url;
|
||||
m_host = url.host();
|
||||
m_port = url.port();
|
||||
|
||||
if (m_port <= 0) {
|
||||
m_port = 22;
|
||||
}
|
||||
|
||||
m_hostPreferences = new SshHostPreferences(configGroup, this);
|
||||
}
|
||||
|
||||
SshView::~SshView()
|
||||
{
|
||||
if (!m_quitting) {
|
||||
startQuitting();
|
||||
}
|
||||
}
|
||||
|
||||
bool SshView::loadKonsolePart()
|
||||
{
|
||||
const auto result = KPluginFactory::instantiatePlugin<KParts::ReadOnlyPart>(
|
||||
KPluginMetaData(QStringLiteral("kf6/parts/konsolepart")));
|
||||
|
||||
if (!result) {
|
||||
qCCritical(KRDC_SSH) << "Failed to load KonsolePart:" << result.errorString;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_terminalPart = result.plugin;
|
||||
m_terminalPart->setParent(this);
|
||||
|
||||
// Embed the terminal widget into our layout
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
layout->addWidget(m_terminalPart->widget());
|
||||
m_terminalPart->widget()->setFocusPolicy(Qt::WheelFocus);
|
||||
setFocusProxy(m_terminalPart->widget());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList SshView::buildSshCommand() const
|
||||
{
|
||||
QStringList args;
|
||||
args << QStringLiteral("ssh");
|
||||
|
||||
// Add port if non-default
|
||||
if (m_port != 22) {
|
||||
args << QStringLiteral("-p") << QString::number(m_port);
|
||||
}
|
||||
|
||||
// Identity file
|
||||
const QString identityFile = m_hostPreferences->identityFile();
|
||||
if (!identityFile.isEmpty()) {
|
||||
args << QStringLiteral("-i") << identityFile;
|
||||
}
|
||||
|
||||
// Options
|
||||
if (m_hostPreferences->x11Forwarding()) {
|
||||
args << QStringLiteral("-X");
|
||||
}
|
||||
if (m_hostPreferences->agentForwarding()) {
|
||||
args << QStringLiteral("-A");
|
||||
}
|
||||
if (m_hostPreferences->compression()) {
|
||||
args << QStringLiteral("-C");
|
||||
}
|
||||
|
||||
// Port forwarding tunnels
|
||||
const QStringList tunnelSpecs = m_hostPreferences->tunnels();
|
||||
for (const QString &tunnel : tunnelSpecs) {
|
||||
// Each tunnel is stored as "-L spec" or "-R spec" or "-D spec"
|
||||
const QStringList parts = tunnel.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.size() == 2) {
|
||||
args << parts.at(0) << parts.at(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Build user@host
|
||||
// Prefer username from preferences, fall back to URL username
|
||||
QString user = m_hostPreferences->userName();
|
||||
if (user.isEmpty()) {
|
||||
user = m_url.userName();
|
||||
}
|
||||
|
||||
QString target;
|
||||
if (!user.isEmpty()) {
|
||||
target = user + QLatin1Char('@') + m_host;
|
||||
} else {
|
||||
target = m_host;
|
||||
}
|
||||
args << target;
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
bool SshView::startConnection()
|
||||
{
|
||||
setStatus(Connecting);
|
||||
|
||||
if (!loadKonsolePart()) {
|
||||
Q_EMIT errorMessage(i18n("SSH Error"),
|
||||
i18n("Could not load the terminal emulator component (KonsolePart). "
|
||||
"Please ensure Konsole is installed."));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *terminalInterface = qobject_cast<TerminalInterface *>(m_terminalPart);
|
||||
if (!terminalInterface) {
|
||||
qCCritical(KRDC_SSH) << "KonsolePart does not implement TerminalInterface";
|
||||
Q_EMIT errorMessage(i18n("SSH Error"),
|
||||
i18n("Terminal component does not support the required interface."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build the SSH command
|
||||
const QStringList sshCommand = buildSshCommand();
|
||||
qCDebug(KRDC_SSH) << "Starting SSH with command:" << sshCommand;
|
||||
|
||||
const QString sshPath = QStandardPaths::findExecutable(QStringLiteral("ssh"));
|
||||
if (sshPath.isEmpty()) {
|
||||
Q_EMIT errorMessage(i18n("SSH Error"),
|
||||
i18n("Could not find the ssh executable in PATH."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Connect to the terminal's finished signal to detect disconnection
|
||||
connect(m_terminalPart, &KParts::ReadOnlyPart::destroyed, this, [this]() {
|
||||
if (!m_quitting) {
|
||||
setStatus(Disconnected);
|
||||
Q_EMIT disconnected();
|
||||
}
|
||||
});
|
||||
|
||||
// Start SSH directly via startProgram for clean process lifecycle.
|
||||
terminalInterface->startProgram(sshPath, sshCommand);
|
||||
|
||||
m_connected = true;
|
||||
setStatus(Connected);
|
||||
Q_EMIT connected();
|
||||
Q_EMIT framebufferSizeChanged(m_framebufferSize.width(), m_framebufferSize.height());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SshView::startQuittingConnection()
|
||||
{
|
||||
m_quitting = true;
|
||||
|
||||
if (m_terminalPart) {
|
||||
// Disconnect signals to avoid callbacks during teardown
|
||||
m_terminalPart->disconnect(this);
|
||||
|
||||
// Use deleteLater to let Qt process pending events for KonsolePart
|
||||
// before destruction. Direct delete causes use-after-free in deferred
|
||||
// child deletions.
|
||||
m_terminalPart->deleteLater();
|
||||
m_terminalPart = nullptr;
|
||||
}
|
||||
|
||||
setStatus(Disconnected);
|
||||
}
|
||||
|
||||
QSize SshView::framebufferSize()
|
||||
{
|
||||
if (m_terminalPart && m_terminalPart->widget()) {
|
||||
return m_terminalPart->widget()->size();
|
||||
}
|
||||
return m_framebufferSize;
|
||||
}
|
||||
|
||||
QSize SshView::sizeHint() const
|
||||
{
|
||||
return m_framebufferSize;
|
||||
}
|
||||
|
||||
QSize SshView::minimumSizeHint() const
|
||||
{
|
||||
return QSize(320, 240);
|
||||
}
|
||||
|
||||
bool SshView::isQuitting()
|
||||
{
|
||||
return m_quitting;
|
||||
}
|
||||
|
||||
bool SshView::supportsScaling() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SshView::supportsLocalCursor() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SshView::supportsViewOnly() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
HostPreferences *SshView::hostPreferences()
|
||||
{
|
||||
return m_hostPreferences;
|
||||
}
|
||||
|
||||
void SshView::scaleResize(int w, int h)
|
||||
{
|
||||
m_framebufferSize = QSize(w, h);
|
||||
// Resize ourselves to fill the scroll area. The m_connected guard in
|
||||
// resizeEvent prevents re-entrant crashes during the show sequence.
|
||||
resize(w, h);
|
||||
}
|
||||
|
||||
void SshView::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
// KonsolePart handles its own painting via its embedded widget.
|
||||
// We just let the base class handle the background if needed.
|
||||
RemoteView::paintEvent(event);
|
||||
}
|
||||
|
||||
void SshView::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
RemoteView::resizeEvent(event);
|
||||
// Layout handles KonsolePart widget sizing.
|
||||
}
|
||||
|
||||
void SshView::handleKeyEvent(QKeyEvent *event)
|
||||
{
|
||||
// Input is handled directly by KonsolePart's widget via focus proxy.
|
||||
Q_UNUSED(event);
|
||||
}
|
||||
|
||||
void SshView::handleWheelEvent(QWheelEvent *event)
|
||||
{
|
||||
// Scrollback handled by KonsolePart directly.
|
||||
Q_UNUSED(event);
|
||||
}
|
||||
|
||||
void SshView::handleMouseEvent(QMouseEvent *event)
|
||||
{
|
||||
// Mouse events handled by KonsolePart directly.
|
||||
Q_UNUSED(event);
|
||||
}
|
||||
|
||||
void SshView::handleLocalClipboardChanged(const QMimeData *data)
|
||||
{
|
||||
// KonsolePart handles clipboard internally.
|
||||
Q_UNUSED(data);
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef SSHVIEW_H
|
||||
#define SSHVIEW_H
|
||||
|
||||
#include "remoteview.h"
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KParts/ReadOnlyPart>
|
||||
|
||||
class SshHostPreferences;
|
||||
|
||||
namespace KParts
|
||||
{
|
||||
class ReadOnlyPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSH terminal view. Embeds KonsolePart to provide a full terminal emulator
|
||||
* connected to a remote host via SSH.
|
||||
*/
|
||||
class SshView : public RemoteView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SshView(QWidget *parent = nullptr, const QUrl &url = QUrl(), KConfigGroup configGroup = KConfigGroup());
|
||||
~SshView() override;
|
||||
|
||||
QSize framebufferSize() override;
|
||||
QSize sizeHint() const override;
|
||||
QSize minimumSizeHint() const override;
|
||||
bool isQuitting() override;
|
||||
bool supportsScaling() const override;
|
||||
bool supportsLocalCursor() const override;
|
||||
bool supportsViewOnly() const override;
|
||||
|
||||
HostPreferences *hostPreferences() override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void scaleResize(int w, int h) override;
|
||||
|
||||
protected:
|
||||
bool startConnection() override;
|
||||
void startQuittingConnection() override;
|
||||
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void handleKeyEvent(QKeyEvent *event) override;
|
||||
void handleWheelEvent(QWheelEvent *event) override;
|
||||
void handleMouseEvent(QMouseEvent *event) override;
|
||||
void handleLocalClipboardChanged(const QMimeData *data) override;
|
||||
|
||||
private:
|
||||
bool loadKonsolePart();
|
||||
QStringList buildSshCommand() const;
|
||||
|
||||
KParts::ReadOnlyPart *m_terminalPart = nullptr;
|
||||
SshHostPreferences *m_hostPreferences = nullptr;
|
||||
bool m_quitting = false;
|
||||
bool m_connected = false;
|
||||
QSize m_framebufferSize;
|
||||
};
|
||||
|
||||
#endif // SSHVIEW_H
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "sshviewfactory.h"
|
||||
#include "sshhostpreferences.h"
|
||||
#include "sshview.h"
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <KPluginFactory>
|
||||
|
||||
K_PLUGIN_CLASS_WITH_JSON(SshViewFactory, "krdc_ssh.json")
|
||||
|
||||
SshViewFactory::SshViewFactory(QObject *parent, const QVariantList &args)
|
||||
: RemoteViewFactory(parent)
|
||||
{
|
||||
Q_UNUSED(args);
|
||||
KLocalizedString::setApplicationDomain("krdc");
|
||||
}
|
||||
|
||||
SshViewFactory::~SshViewFactory()
|
||||
{
|
||||
}
|
||||
|
||||
bool SshViewFactory::supportsUrl(const QUrl &url) const
|
||||
{
|
||||
return (url.scheme().compare(QLatin1String("ssh"), Qt::CaseInsensitive) == 0);
|
||||
}
|
||||
|
||||
RemoteView *SshViewFactory::createView(QWidget *parent, const QUrl &url, KConfigGroup configGroup)
|
||||
{
|
||||
return new SshView(parent, url, configGroup);
|
||||
}
|
||||
|
||||
HostPreferences *SshViewFactory::createHostPreferences(KConfigGroup configGroup, QWidget *parent)
|
||||
{
|
||||
return new SshHostPreferences(configGroup, parent);
|
||||
}
|
||||
|
||||
QString SshViewFactory::scheme() const
|
||||
{
|
||||
return QLatin1String("ssh");
|
||||
}
|
||||
|
||||
QString SshViewFactory::connectActionText() const
|
||||
{
|
||||
return i18n("New SSH Connection\u2026");
|
||||
}
|
||||
|
||||
QString SshViewFactory::connectButtonText() const
|
||||
{
|
||||
return i18n("Connect to an SSH Terminal");
|
||||
}
|
||||
|
||||
QString SshViewFactory::connectToolTipText() const
|
||||
{
|
||||
return i18n(
|
||||
"<html>Enter the address here.<br />"
|
||||
"<i>Example: user@server (or server:port)</i></html>");
|
||||
}
|
||||
|
||||
#include "sshviewfactory.moc"
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
SPDX-FileCopyrightText: 2024 KRDC SSH Plugin Authors
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef SSHVIEWFACTORY_H
|
||||
#define SSHVIEWFACTORY_H
|
||||
|
||||
#include "remoteviewfactory.h"
|
||||
|
||||
class SshViewFactory : public RemoteViewFactory
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SshViewFactory(QObject *parent, const QVariantList &args);
|
||||
~SshViewFactory() override;
|
||||
|
||||
bool supportsUrl(const QUrl &url) const override;
|
||||
RemoteView *createView(QWidget *parent, const QUrl &url, KConfigGroup configGroup) override;
|
||||
HostPreferences *createHostPreferences(KConfigGroup configGroup, QWidget *parent) override;
|
||||
QString scheme() const override;
|
||||
QString connectActionText() const override;
|
||||
QString connectButtonText() const override;
|
||||
QString connectToolTipText() const override;
|
||||
};
|
||||
|
||||
#endif // SSHVIEWFACTORY_H
|
||||
Loading…
Reference in New Issue