diff --git a/CMakeLists.txt b/CMakeLists.txt
index ca790783..caba3e9d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,7 +23,7 @@ if(NOT CMAKE_BUILD_TYPE)
)
endif()
-set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(APPIMAGE_BUILD "configure build for bundling in an appimage" OFF)
@@ -58,6 +58,13 @@ find_package(
REQUIRED
)
+find_package(
+ QCoro${QT_MAJOR_VERSION}
+ COMPONENTS Core
+ REQUIRED
+)
+qcoro_enable_coroutines()
+
find_package(LibElf REQUIRED)
find_package(ElfUtils REQUIRED)
find_package(ECM 1.0.0 NO_MODULE REQUIRED)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 66e453ab..11212e10 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -55,6 +55,7 @@ set(HOTSPOT_SRCS
errnoutil.cpp
recordhost.cpp
copyabletreeview.cpp
+ remotedevice.cpp
# ui files:
mainwindow.ui
aboutdialog.ui
@@ -119,6 +120,7 @@ target_link_libraries(
models
PrefixTickLabels
KDAB::kddockwidgets
+ QCoro::Core
)
if(KFArchive_FOUND)
diff --git a/src/devicesettings.ui b/src/devicesettings.ui
new file mode 100644
index 00000000..06ba06c0
--- /dev/null
+++ b/src/devicesettings.ui
@@ -0,0 +1,124 @@
+
+
+ DeviceSettings
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ Hostname:
+
+
+
+ -
+
+
+ -
+
+
+ Username:
+
+
+
+ -
+
+
+ -
+
+
+ Password:
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QLineEdit::Password
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Copy SSH Key
+
+
+
+
+
+
+ -
+
+
+ Options:
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/src/multiconfigwidget.cpp b/src/multiconfigwidget.cpp
index 5225111f..29d572b6 100644
--- a/src/multiconfigwidget.cpp
+++ b/src/multiconfigwidget.cpp
@@ -1,119 +1,208 @@
/*
SPDX-FileCopyrightText: Lieven Hey
- SPDX-FileCopyrightText: Milian Wolff
- SPDX-FileCopyrightText: 2016-2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+ SPDX-FileCopyrightText: 2022-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "multiconfigwidget.h"
+#include
+
#include
+#include
#include
#include
#include
+#include
+
+#include "ui_multiconfigwidget.h"
+
MultiConfigWidget::MultiConfigWidget(QWidget* parent)
: QWidget(parent)
+ , m_configWidget(new Ui::MultiConfigWidget)
{
- auto* layout = new QHBoxLayout(this);
- layout->setContentsMargins(0, 0, 0, 0);
-
- m_comboBox = new QComboBox(this);
- m_comboBox->setEditable(true);
- m_comboBox->setInsertPolicy(QComboBox::InsertAtCurrent);
- m_comboBox->setDisabled(true);
- layout->addWidget(m_comboBox);
-
- connect(m_comboBox->lineEdit(), &QLineEdit::editingFinished, this, [this] {
- m_config.deleteGroup(m_comboBox->currentData().toString());
- saveConfigAs(m_comboBox->currentText());
- m_comboBox->setItemData(m_comboBox->currentIndex(), m_comboBox->currentText());
- m_config.sync();
- });
+ m_configWidget->setupUi(this);
+
+ connect(m_configWidget->currentConfigComboBox, qOverload(&QComboBox::currentIndexChanged), this,
+ [this](int) { loadConfig(currentConfig()); });
- connect(m_comboBox, QOverload::of(&QComboBox::currentIndexChanged), this,
- [this] { selectConfig(m_comboBox->currentData().toString()); });
+ connect(this, &MultiConfigWidget::configsChanged, this, [this]() {
+ m_configWidget->currentConfigComboBox->setDisabled(m_configWidget->currentConfigComboBox->count() == 0);
+ });
- m_copyButton = new QPushButton(this);
- m_copyButton->setText(tr("Copy Config"));
- layout->addWidget(m_copyButton);
+ m_configWidget->currentConfigComboBox->setEnabled(true);
- connect(m_copyButton, &QPushButton::clicked, this, [this] {
- const QString name = tr("Config %1").arg(m_comboBox->count() + 1);
- saveConfigAs(name);
- m_comboBox->addItem(name, name);
- m_comboBox->setDisabled(false);
+ connect(m_configWidget->copyButton, &QPushButton::pressed, this, [this] {
+ const auto name = m_configWidget->currentConfigComboBox->currentText();
+ auto copyName = QStringLiteral("Copy of %1").arg(name);
+ if (name.isEmpty()) {
+ copyName = QStringLiteral("Config");
+ }
+ saveConfig(copyName);
+ m_configWidget->currentConfigComboBox->addItem(copyName, copyName);
+ m_configWidget->currentConfigComboBox->setCurrentIndex(m_configWidget->currentConfigComboBox->count() - 1);
+ emit configsChanged();
});
- m_removeButton = new QPushButton(this);
- m_removeButton->setText(tr("Remove Config"));
- layout->addWidget(m_removeButton);
+ connect(m_configWidget->deleteButton, &QPushButton::pressed, this, [this] {
+ const auto currentConfig = m_configWidget->currentConfigComboBox->currentText();
+ m_configWidget->currentConfigComboBox->removeItem(m_configWidget->currentConfigComboBox->currentIndex());
+ m_group.deleteGroup(currentConfig);
+ emit configsChanged();
+ });
- connect(m_removeButton, &QPushButton::clicked, this, [this] {
- m_config.deleteGroup(m_comboBox->currentData().toString());
- m_comboBox->removeItem(m_comboBox->currentIndex());
+ connect(m_configWidget->currentConfigComboBox->lineEdit(), &QLineEdit::returnPressed, this, [this] {
+ const auto oldName = m_configWidget->currentConfigComboBox->currentData().toString();
+ if (!oldName.isEmpty()) {
+ m_group.deleteGroup(oldName);
+ }
- if (m_comboBox->count() == 0) {
- m_comboBox->setDisabled(true);
- } else {
- selectConfig(m_comboBox->currentData().toString());
+ auto newName = m_configWidget->currentConfigComboBox->currentText();
+ if (newName.isEmpty()) {
+ newName = QStringLiteral("Config %1").arg(m_configWidget->currentConfigComboBox->currentIndex());
}
+ m_configWidget->currentConfigComboBox->setItemData(m_configWidget->currentConfigComboBox->currentIndex(),
+ newName);
+ saveConfig(newName);
});
-
- setLayout(layout);
}
MultiConfigWidget::~MultiConfigWidget() = default;
-QString MultiConfigWidget::currentConfig() const
+void MultiConfigWidget::setChildWidget(QWidget* widget, const QVector& formWidgets)
{
- return m_comboBox->currentData().toString();
+ m_formWidgets = formWidgets;
+ m_child = widget;
+ m_child->setParent(this);
+ auto placeholder = m_configWidget->layout->replaceWidget(m_configWidget->placeholder, m_child);
+ Q_ASSERT(placeholder);
+ delete placeholder;
}
-void MultiConfigWidget::setConfig(const KConfigGroup& group)
+void MultiConfigWidget::setConfigGroup(const KConfigGroup& group)
{
- m_comboBox->clear();
- m_config = group;
+ m_group = group;
+ if (!m_group.isValid()) {
+ return;
+ }
+
+ m_configWidget->currentConfigComboBox->clear();
+ auto configGroups = configs();
+ for (const auto& config : configGroups) {
+ m_configWidget->currentConfigComboBox->addItem(config, config);
+ }
+
+ m_configWidget->currentConfigComboBox->setCurrentIndex(0);
- if (!m_config.isValid())
+ if (!configGroups.isEmpty()) {
+ loadConfig(currentConfig());
+ }
+ emit configsChanged();
+}
+
+void MultiConfigWidget::loadConfig(const QString& name)
+{
+ if (m_saving) {
return;
+ }
- const auto groups = m_config.groupList();
- for (const auto& config : groups) {
- if (m_config.hasGroup(config)) {
- // item data is used to get the old name after renaming
- m_comboBox->addItem(config, config);
- m_comboBox->setDisabled(false);
+ if (m_child == nullptr) {
+ return;
+ }
+
+ if (name.isEmpty() || !m_group.isValid()) {
+ return;
+ }
+
+ if (!m_group.hasGroup(name)) {
+ return;
+ }
+
+ const auto& group = m_group.group(name);
+
+ for (auto formWidget : std::as_const(m_formWidgets)) {
+ const auto& name = formWidget->objectName();
+ if (auto widget = qobject_cast(formWidget)) {
+ auto text = group.readEntry(name, QString {});
+ widget->setText(text);
+ } else if (auto widget = qobject_cast(formWidget)) {
+ auto text = group.readEntry(name, QString {});
+ widget->setText(text);
+ } else if (auto widget = qobject_cast(formWidget)) {
+ auto items = group.readEntry(name, QString {}).split(QLatin1Char(':'));
+ widget->setItems(items);
+ } else if (auto widget = qobject_cast(formWidget)) {
+ auto value = group.readEntry(name, QString {});
+ if (value.isEmpty()) {
+ continue;
+ }
+ auto index = widget->findText(value);
+ if (index == -1) {
+ index = widget->count();
+ widget->addItem(value);
+ }
+ widget->setCurrentIndex(index);
+ } else {
+ qWarning() << formWidget->metaObject()->className() << "is not supported in MultiConfigWidget";
}
}
}
-void MultiConfigWidget::saveConfigAs(const QString& name)
+void MultiConfigWidget::saveConfig(const QString& name)
{
- if (!name.isEmpty()) {
- emit saveConfig(m_config.group(name));
+ if (!m_child) {
+ return;
+ }
+
+ if (name.isEmpty() || !m_group.isValid()) {
+ return;
+ }
+
+ m_saving = true;
+
+ auto group = m_group.group(name);
+
+ for (const auto formWidget : std::as_const(m_formWidgets)) {
+ const auto& name = formWidget->objectName();
+ if (auto widget = qobject_cast(formWidget)) {
+ group.writeEntry(name, widget->text());
+ } else if (auto widget = qobject_cast(formWidget)) {
+ group.writeEntry(name, widget->text());
+ } else if (auto widget = qobject_cast(formWidget)) {
+ auto data = widget->items().join(QLatin1Char(':'));
+ group.writeEntry(name, data);
+ } else if (auto widget = qobject_cast(formWidget)) {
+ group.writeEntry(name, widget->currentText());
+ } else {
+ qWarning() << formWidget->metaObject()->className() << "is not supported in MultiConfigWidget";
+ }
}
+
+ m_saving = false;
}
-void MultiConfigWidget::updateCurrentConfig()
+void MultiConfigWidget::saveCurrentConfig()
{
- if (m_comboBox->currentIndex() != -1) {
- saveConfigAs(m_comboBox->currentData().toString());
- }
+ saveConfig(currentConfig());
}
-void MultiConfigWidget::selectConfig(const QString& name)
+QString MultiConfigWidget::currentConfig() const
{
- m_config.sync();
- if (!name.isEmpty() && m_config.hasGroup(name)) {
- emit restoreConfig(m_config.group(name));
- }
+ return m_configWidget->currentConfigComboBox->currentText();
}
-void MultiConfigWidget::restoreCurrent()
+QStringList MultiConfigWidget::configs() const
{
- if (m_comboBox->currentIndex() != -1) {
- selectConfig(m_comboBox->currentData().toString());
- }
+ auto configs = m_group.groupList();
+ // KConfig is weird in regards to deleted groups
+ // they are still in groupList but are no longer valid
+ // TODO: C++20 use filter
+ configs.erase(std::remove_if(configs.begin(), configs.end(),
+ [group = m_group](const QString& groupName) {
+ return !group.hasGroup(groupName) || !group.group(groupName).exists();
+ }),
+ configs.end());
+ return configs;
}
diff --git a/src/multiconfigwidget.h b/src/multiconfigwidget.h
index 5f7b71f0..d51cc9a2 100644
--- a/src/multiconfigwidget.h
+++ b/src/multiconfigwidget.h
@@ -1,7 +1,6 @@
/*
SPDX-FileCopyrightText: Lieven Hey
- SPDX-FileCopyrightText: Milian Wolff
- SPDX-FileCopyrightText: 2016-2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+ SPDX-FileCopyrightText: 2022-2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
SPDX-License-Identifier: GPL-2.0-or-later
*/
@@ -11,32 +10,46 @@
#include
#include
-class QComboBox;
-class QPushButton;
+namespace Ui {
+class MultiConfigWidget;
+}
+/** this widget allows fast switching between different configurations
+ * of m_child
+ * saveConfig will automatically save all changes to m_group
+ * use setChildWidget for an unparented one and moveWidgetToChild for a parented one
+ * */
class MultiConfigWidget : public QWidget
{
Q_OBJECT
public:
MultiConfigWidget(QWidget* parent = nullptr);
- ~MultiConfigWidget();
+ ~MultiConfigWidget() override;
- QString currentConfig() const;
+ /** widget is the widget containing the form which will be shown inside the MultiConfigWidget
+ * formWidgets is a list of all user editable widgets in widget that will automatically be saved and restored
+ * each widget in formWidgets needs a unique name **/
+ void setChildWidget(QWidget* widget, const QVector& formWidgets);
-signals:
- void saveConfig(KConfigGroup group);
- void restoreConfig(const KConfigGroup& group);
+ /** set group where everything should be saved in **/
+ void setConfigGroup(const KConfigGroup& group);
+
+ QString currentConfig() const;
public slots:
- void setConfig(const KConfigGroup& group);
- void saveConfigAs(const QString& name);
- void updateCurrentConfig();
- void selectConfig(const QString& name);
- void restoreCurrent();
+ void loadConfig(const QString& name);
+ void saveConfig(const QString& name);
+ void saveCurrentConfig();
+
+signals:
+ void configsChanged();
+ void currentConfigChanged();
private:
- KConfigGroup m_config;
- QComboBox* m_comboBox = nullptr;
- QPushButton* m_copyButton = nullptr;
- QPushButton* m_removeButton = nullptr;
+ QStringList configs() const;
+ std::unique_ptr m_configWidget;
+ QWidget* m_child = nullptr;
+ QVector m_formWidgets;
+ KConfigGroup m_group;
+ bool m_saving = false;
};
diff --git a/src/multiconfigwidget.ui b/src/multiconfigwidget.ui
new file mode 100644
index 00000000..736c75e8
--- /dev/null
+++ b/src/multiconfigwidget.ui
@@ -0,0 +1,115 @@
+
+
+ MultiConfigWidget
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ true
+
+
+ true
+
+
+ QComboBox::InsertAtCurrent
+
+
+ QComboBox::AdjustToContents
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Copy
+
+
+
+ -
+
+
+ Delete
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/src/perfrecord.cpp b/src/perfrecord.cpp
index e3adc589..f04f5c76 100644
--- a/src/perfrecord.cpp
+++ b/src/perfrecord.cpp
@@ -21,6 +21,7 @@
#include
#include
+#include
#include
#if KWINDOWSYSTEM_VERSION >= QT_VERSION_CHECK(5, 101, 0)
#include
@@ -57,7 +58,6 @@ PerfRecord::~PerfRecord()
stopRecording();
if (m_perfRecordProcess) {
m_perfRecordProcess->waitForFinished(100);
- delete m_perfRecordProcess;
}
}
@@ -69,62 +69,31 @@ bool PerfRecord::runPerf(bool elevatePrivileges, const QStringList& perfOptions,
m_perfControlFifo.requestStop();
m_perfControlFifo.close();
m_perfRecordProcess->kill();
- m_perfRecordProcess->deleteLater();
}
- m_perfRecordProcess = new QProcess(this);
- m_perfRecordProcess->setProcessChannelMode(QProcess::MergedChannels);
- const auto outputFileInfo = QFileInfo(outputPath);
- const auto folderPath = outputFileInfo.dir().path();
- const auto folderInfo = QFileInfo(folderPath);
- if (!folderInfo.exists()) {
- emit recordingFailed(tr("Folder '%1' does not exist.").arg(folderPath));
- return false;
- }
- if (!folderInfo.isDir()) {
- emit recordingFailed(tr("'%1' is not a folder.").arg(folderPath));
- return false;
- }
- if (!folderInfo.isWritable()) {
- emit recordingFailed(tr("Folder '%1' is not writable.").arg(folderPath));
- return false;
- }
-
- connect(m_perfRecordProcess.data(), static_cast(&QProcess::finished),
- this, [this](int exitCode, QProcess::ExitStatus exitStatus) {
- Q_UNUSED(exitStatus)
+ m_outputPath = outputPath;
+ m_userTerminated = false;
- const auto outputFileInfo = QFileInfo(m_outputPath);
- if ((exitCode == EXIT_SUCCESS || (exitCode == SIGTERM && m_userTerminated) || outputFileInfo.size() > 0)
- && outputFileInfo.exists()) {
- if (exitCode != EXIT_SUCCESS && !m_userTerminated) {
- emit debuggeeCrashed();
- }
- emit recordingFinished(m_outputPath);
- } else {
- emit recordingFailed(tr("Failed to record perf data, error code %1.").arg(exitCode));
- }
- m_userTerminated = false;
- });
+ if (m_host->isLocal()) {
+ return runPerfLocal(elevatePrivileges, perfOptions, outputPath, workingDirectory);
+ } else {
+ return runPerfRemote(perfOptions, outputPath, workingDirectory);
+ }
+}
- connect(m_perfRecordProcess.data(), &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
- Q_UNUSED(error)
- if (!m_userTerminated) {
- emit recordingFailed(m_perfRecordProcess->errorString());
- }
- });
+bool PerfRecord::runPerfLocal(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath,
+ const QString& workingDirectory)
+{
+ m_perfRecordProcess = std::make_unique(this);
+ m_perfRecordProcess->setProcessChannelMode(QProcess::MergedChannels);
- connect(m_perfRecordProcess.data(), &QProcess::started, this,
- [this] { emit recordingStarted(m_perfRecordProcess->program(), m_perfRecordProcess->arguments()); });
+ connectRecordingProcessErrors();
- connect(m_perfRecordProcess.data(), &QProcess::readyRead, this, [this]() {
+ connect(m_perfRecordProcess.get(), &QProcess::readyRead, this, [this]() {
const auto output = QString::fromUtf8(m_perfRecordProcess->readAll());
emit recordingOutput(output);
});
- m_outputPath = outputPath;
- m_userTerminated = false;
-
if (!workingDirectory.isEmpty()) {
m_perfRecordProcess->setWorkingDirectory(workingDirectory);
}
@@ -161,6 +130,36 @@ bool PerfRecord::runPerf(bool elevatePrivileges, const QStringList& perfOptions,
return true;
}
+bool PerfRecord::runPerfRemote(const QStringList& perfOptions, const QString& outputPath,
+ const QString& workingDirectory)
+{
+ m_perfRecordProcess = m_host->remoteDevice()->runPerf(workingDirectory, perfOptions);
+
+ auto output = new QFile(outputPath, m_perfRecordProcess.get());
+ if (!output->open(QIODevice::WriteOnly)) {
+ emit recordingFailed(QStringLiteral("Failed to create output file: %1").arg(outputPath));
+ return false;
+ }
+
+ connect(m_perfRecordProcess.get(), &QProcess::readyReadStandardOutput, m_perfRecordProcess.get(),
+ [process = m_perfRecordProcess.get(), output] {
+ auto data = process->readAllStandardOutput();
+ output->write(data);
+ });
+ connect(m_perfRecordProcess.get(), &QProcess::readyReadStandardError, m_perfRecordProcess.get(),
+ [this] { emit recordingOutput(QString::fromUtf8(m_perfRecordProcess->readAllStandardError())); });
+
+ connect(m_perfRecordProcess.get(), qOverload(&QProcess::finished), this, [output] {
+ output->close();
+ output->deleteLater();
+ });
+
+ connectRecordingProcessErrors();
+
+ m_perfRecordProcess->start();
+ return true;
+}
+
void PerfRecord::record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
const QStringList& pids)
{
@@ -174,27 +173,11 @@ void PerfRecord::record(const QStringList& perfOptions, const QString& outputPat
runPerf(actuallyElevatePrivileges(elevatePrivileges), options, outputPath, {});
}
-void PerfRecord::record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
- const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory)
+void PerfRecord::record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges)
{
- QFileInfo exeFileInfo(exePath);
-
- if (!exeFileInfo.exists()) {
- exeFileInfo.setFile(QStandardPaths::findExecutable(exePath));
- }
-
- if (!exeFileInfo.exists()) {
- emit recordingFailed(tr("File '%1' does not exist.").arg(exePath));
- return;
- }
- if (!exeFileInfo.isFile()) {
- emit recordingFailed(tr("'%1' is not a file.").arg(exePath));
- return;
- }
- if (!exeFileInfo.isExecutable()) {
- emit recordingFailed(tr("File '%1' is not executable.").arg(exePath));
- return;
- }
+ auto exePath = m_host->clientApplication();
+ auto exeOptions = m_host->clientApplicationArguments();
+ auto workingDirectory = m_host->currentWorkingDirectory();
QStringList options = perfOptions;
if (actuallyElevatePrivileges(elevatePrivileges)) {
@@ -211,7 +194,7 @@ void PerfRecord::record(const QStringList& perfOptions, const QString& outputPat
m_perfControlFifo.requestStart();
} else {
- options.append(exeFileInfo.absoluteFilePath());
+ options.append(exePath);
options += exeOptions;
runPerf(false, options, outputPath, workingDirectory);
}
@@ -262,3 +245,33 @@ bool PerfRecord::actuallyElevatePrivileges(bool elevatePrivileges) const
const auto capabilities = m_host->perfCapabilities();
return elevatePrivileges && capabilities.canElevatePrivileges && !capabilities.privilegesAlreadyElevated;
}
+
+void PerfRecord::connectRecordingProcessErrors()
+{
+ connect(m_perfRecordProcess.get(), qOverload(&QProcess::finished), this,
+ [this](int exitCode, QProcess::ExitStatus exitStatus) {
+ Q_UNUSED(exitStatus)
+
+ const auto outputFileInfo = QFileInfo(m_outputPath);
+ if ((exitCode == EXIT_SUCCESS || (exitCode == SIGTERM && m_userTerminated) || outputFileInfo.size() > 0)
+ && outputFileInfo.exists()) {
+ if (exitCode != EXIT_SUCCESS && !m_userTerminated) {
+ emit debuggeeCrashed();
+ }
+ emit recordingFinished(m_outputPath);
+ } else {
+ emit recordingFailed(tr("Failed to record perf data, error code %1.").arg(exitCode));
+ }
+ m_userTerminated = false;
+ });
+
+ connect(m_perfRecordProcess.get(), &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
+ Q_UNUSED(error)
+ if (!m_userTerminated) {
+ emit recordingFailed(m_perfRecordProcess->errorString());
+ }
+ });
+
+ connect(m_perfRecordProcess.get(), &QProcess::started, this,
+ [this] { emit recordingStarted(m_perfRecordProcess->program(), m_perfRecordProcess->arguments()); });
+}
diff --git a/src/perfrecord.h b/src/perfrecord.h
index ebcc4ce2..429dc1d5 100644
--- a/src/perfrecord.h
+++ b/src/perfrecord.h
@@ -12,9 +12,10 @@
#include "perfcontrolfifowrapper.h"
#include
-#include
+#include
+
+#include
-class QProcess;
class RecordHost;
class PerfRecord : public QObject
@@ -24,8 +25,8 @@ class PerfRecord : public QObject
explicit PerfRecord(const RecordHost* host, QObject* parent = nullptr);
~PerfRecord();
- void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
- const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory = QString());
+ void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges);
+
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
const QStringList& pids);
void recordSystem(const QStringList& perfOptions, const QString& outputPath);
@@ -45,7 +46,7 @@ class PerfRecord : public QObject
private:
const RecordHost* m_host = nullptr;
- QPointer m_perfRecordProcess;
+ std::unique_ptr m_perfRecordProcess;
InitiallyStoppedProcess m_targetProcessForPrivilegedPerf;
PerfControlFifoWrapper m_perfControlFifo;
QString m_outputPath;
@@ -55,4 +56,13 @@ class PerfRecord : public QObject
bool runPerf(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath,
const QString& workingDirectory = QString());
+
+ bool runPerfLocal(bool elevatePrivileges, const QStringList& perfOptions, const QString& outputPath,
+ const QString& workingDirectory = QString());
+ bool runPerfRemote(const QStringList& perfOptions, const QString& outputPath,
+ const QString& workingDirectory = QString());
+
+ bool runRemotePerf(const QStringList& perfOptions, const QString& outputPath, const QString& workingDirectory = {});
+
+ void connectRecordingProcessErrors();
};
diff --git a/src/recordhost.cpp b/src/recordhost.cpp
index e588948c..ff503b0c 100644
--- a/src/recordhost.cpp
+++ b/src/recordhost.cpp
@@ -11,6 +11,9 @@
#include
#include
#include
+#include
+
+#include
#include
#include
@@ -142,6 +145,28 @@ RecordHost::PerfCapabilities fetchLocalPerfCapabilities(const QString& perfPath)
return capabilities;
}
+
+RecordHost::PerfCapabilities fetchRemotePerfCapabilities(const std::unique_ptr& device)
+{
+ RecordHost::PerfCapabilities capabilities;
+
+ const auto buildOptions = device->getProgramOutput(
+ {QStringLiteral("perf"), QStringLiteral("version"), QStringLiteral("--build-options")});
+ const auto help = device->getProgramOutput({QStringLiteral("perf"), QStringLiteral("--help")});
+
+ capabilities.canCompress = Zstd_FOUND && buildOptions.contains("zszd: [ on ]");
+ capabilities.canSwitchEvents = help.contains("--switch-events");
+ capabilities.canSampleCpu = help.contains("--sample-cpu");
+
+ // TODO: implement
+ capabilities.canProfileOffCpu = false;
+ capabilities.privilegesAlreadyElevated = false;
+
+ capabilities.canUseAio = false; // AIO doesn't work with perf streaming
+ capabilities.canElevatePrivileges = false; // we currently don't support this
+
+ return capabilities;
+}
}
RecordHost::RecordHost(QObject* parent)
@@ -171,7 +196,13 @@ bool RecordHost::isReady() const
{
switch (m_recordType) {
case RecordType::LaunchApplication:
- // client application is already validated in the setter
+ // client application is already validated in the setter
+ if (m_clientApplication.isEmpty() && m_cwd.isEmpty())
+ return false;
+ break;
+ case RecordType::LaunchRemoteApplication:
+ if (!m_remoteDevice || !m_remoteDevice->isConnected())
+ return false;
if (m_clientApplication.isEmpty() && m_cwd.isEmpty())
return false;
break;
@@ -205,6 +236,17 @@ void RecordHost::setHost(const QString& host)
m_host = host;
emit hostChanged();
+ if (!isLocal()) {
+ m_remoteDevice = std::make_unique(this);
+
+ connect(m_remoteDevice.get(), &RemoteDevice::connected, this, &RecordHost::checkRequirements);
+ connect(m_remoteDevice.get(), &RemoteDevice::connected, this, [this] { emit isReadyChanged(isReady()); });
+ connect(m_remoteDevice.get(), &RemoteDevice::failedToConnect, this,
+ [this] { emit errorOccurred(tr("Failed to connect to: %1").arg(m_host)); });
+ } else {
+ m_remoteDevice = nullptr;
+ }
+
// invalidate everything
m_cwd.clear();
emit currentWorkingDirectoryChanged(m_cwd);
@@ -212,38 +254,18 @@ void RecordHost::setHost(const QString& host)
m_clientApplication.clear();
emit clientApplicationChanged(m_clientApplication);
+ m_clientApplicationArguments.clear();
+ emit clientApplicationArgumentsChanged(m_clientApplicationArguments);
+
m_perfCapabilities = {};
emit perfCapabilitiesChanged(m_perfCapabilities);
- const auto perfPath = perfBinaryPath();
- m_checkPerfCapabilitiesJob.startJob([perfPath](auto&&) { return fetchLocalPerfCapabilities(perfPath); },
- [this](RecordHost::PerfCapabilities capabilities) {
- Q_ASSERT(QThread::currentThread() == thread());
-
- m_perfCapabilities = capabilities;
- emit perfCapabilitiesChanged(m_perfCapabilities);
- });
-
- m_checkPerfInstalledJob.startJob(
- [isLocal = isLocal(), perfPath](auto&&) {
- if (isLocal) {
- if (perfPath.isEmpty()) {
- return !QStandardPaths::findExecutable(QStringLiteral("perf")).isEmpty();
- }
-
- return QFileInfo::exists(perfPath);
- }
-
- qWarning() << "remote is not implemented";
- return false;
- },
- [this](bool isInstalled) {
- if (!isInstalled) {
- emit errorOccurred(tr("perf is not installed"));
- }
- m_isPerfInstalled = isInstalled;
- emit isPerfInstalledChanged(isInstalled);
- });
+ if (isLocal()) {
+ checkRequirements();
+ } else {
+ // checkRequirements will be called via RemoteDevice::connected
+ m_remoteDevice->connectToDevice(m_host);
+ }
}
void RecordHost::setCurrentWorkingDirectory(const QString& cwd)
@@ -264,16 +286,29 @@ void RecordHost::setCurrentWorkingDirectory(const QString& cwd)
m_cwd = cwd;
emit currentWorkingDirectoryChanged(cwd);
}
- return;
+ } else {
+ QTimer::singleShot(0, this, [this, cwd]() -> QCoro::Task {
+ bool exists = co_await m_remoteDevice->checkIfDirectoryExists(cwd);
+ if (!exists) {
+ emit errorOccurred(tr("Working directory folder cannot be found: %1").arg(cwd));
+ } else {
+ emit errorOccurred({});
+ m_cwd = cwd;
+ emit currentWorkingDirectoryChanged(m_cwd);
+ }
+ co_return;
+ });
}
-
- qWarning() << "is not implemented for remote";
}
void RecordHost::setClientApplication(const QString& clientApplication)
{
Q_ASSERT(QThread::currentThread() == thread());
+ if (m_clientApplication == clientApplication) {
+ return;
+ }
+
if (isLocal()) {
QFileInfo application(KShell::tildeExpand(clientApplication));
if (!application.exists()) {
@@ -295,38 +330,55 @@ void RecordHost::setClientApplication(const QString& clientApplication)
if (m_cwd.isEmpty()) {
setCurrentWorkingDirectory(application.dir().absolutePath());
}
- return;
+ } else {
+ if (!m_remoteDevice->isConnected()) {
+ emit errorOccurred(tr("Hotspot is not connected to the remote device"));
+ } else {
+ QTimer::singleShot(0, this, [this, clientApplication]() -> QCoro::Task {
+ bool exists = co_await m_remoteDevice->checkIfFileExists(clientApplication);
+ if (!exists) {
+ emit errorOccurred(tr("Application file cannot be found: %1").arg(clientApplication));
+ } else {
+ emit errorOccurred({});
+ m_clientApplication = clientApplication;
+ emit clientApplicationChanged(m_clientApplication);
+ }
+ });
+ }
}
-
- qWarning() << "is not implemented for remote";
}
-void RecordHost::setOutputFileName(const QString& filePath)
+void RecordHost::setClientApplicationArguments(const QStringList& arguments)
{
- if (isLocal()) {
- const auto perfDataExtension = QStringLiteral(".data");
-
- const QFileInfo file(filePath);
- const QFileInfo folder(file.absolutePath());
-
- if (!folder.exists()) {
- emit errorOccurred(tr("Output file directory folder cannot be found: %1").arg(folder.path()));
- } else if (!folder.isDir()) {
- emit errorOccurred(tr("Output file directory folder is not valid: %1").arg(folder.path()));
- } else if (!folder.isWritable()) {
- emit errorOccurred(tr("Output file directory folder is not writable: %1").arg(folder.path()));
- } else if (!file.absoluteFilePath().endsWith(perfDataExtension)) {
- emit errorOccurred(tr("Output file must end with %1").arg(perfDataExtension));
- } else {
- emit errorOccurred({});
- m_outputFileName = filePath;
- emit outputFileNameChanged(m_outputFileName);
- }
+ Q_ASSERT(QThread::currentThread() == thread());
- return;
+ if (m_clientApplicationArguments != arguments) {
+ m_clientApplicationArguments = arguments;
+ emit clientApplicationArgumentsChanged(m_clientApplicationArguments);
}
+}
- qWarning() << "is not implemented for remote";
+void RecordHost::setOutputFileName(const QString& filePath)
+{
+ const auto perfDataExtension = QStringLiteral(".data");
+ const QFileInfo file(filePath);
+ const QFileInfo folder(file.absolutePath());
+
+ // the recording data is streamed from the device (currently) so there is no need to use different logic for
+ // local vs remote
+ if (!folder.exists()) {
+ emit errorOccurred(tr("Output file directory folder cannot be found: %1").arg(folder.path()));
+ } else if (!folder.isDir()) {
+ emit errorOccurred(tr("Output file directory folder is not valid: %1").arg(folder.path()));
+ } else if (!folder.isWritable()) {
+ emit errorOccurred(tr("Output file directory folder is not writable: %1").arg(folder.path()));
+ } else if (!file.absoluteFilePath().endsWith(perfDataExtension)) {
+ emit errorOccurred(tr("Output file must end with %1").arg(perfDataExtension));
+ } else {
+ emit errorOccurred({});
+ m_outputFileName = filePath;
+ emit outputFileNameChanged(m_outputFileName);
+ }
}
void RecordHost::setRecordType(RecordType type)
@@ -368,3 +420,53 @@ QString RecordHost::perfBinaryPath() const
}
return {};
}
+
+void RecordHost::checkRequirements()
+{
+ const auto perfPath = perfBinaryPath();
+ m_checkPerfCapabilitiesJob.startJob(
+ [isLocal = isLocal(), &remoteDevice = m_remoteDevice, perfPath](auto&&) {
+ if (isLocal) {
+ return fetchLocalPerfCapabilities(perfPath);
+ } else {
+ return fetchRemotePerfCapabilities(remoteDevice);
+ }
+ },
+ [this](RecordHost::PerfCapabilities capabilities) {
+ Q_ASSERT(QThread::currentThread() == thread());
+
+ m_perfCapabilities = capabilities;
+ emit perfCapabilitiesChanged(m_perfCapabilities);
+ });
+
+ m_checkPerfInstalledJob.startJob(
+ [isLocal = isLocal(), &remoteDevice = m_remoteDevice, perfPath](auto&&) {
+ if (isLocal) {
+ if (perfPath.isEmpty()) {
+ return !QStandardPaths::findExecutable(QStringLiteral("perf")).isEmpty();
+ }
+
+ return QFileInfo::exists(perfPath);
+ } else {
+ return remoteDevice->checkIfProgramExists(QStringLiteral("perf"));
+ }
+
+ return false;
+ },
+ [this](bool isInstalled) {
+ if (!isInstalled) {
+ emit errorOccurred(tr("perf is not installed"));
+ }
+ m_isPerfInstalled = isInstalled;
+ emit isPerfInstalledChanged(isInstalled);
+ });
+}
+
+void RecordHost::disconnectFromDevice()
+{
+ if (!isLocal()) {
+ if (m_remoteDevice->isConnected()) {
+ m_remoteDevice->disconnect();
+ }
+ }
+}
diff --git a/src/recordhost.h b/src/recordhost.h
index c914193c..6bdce773 100644
--- a/src/recordhost.h
+++ b/src/recordhost.h
@@ -8,12 +8,16 @@
#pragma once
#include "jobtracker.h"
+#include "remotedevice.h"
#include
+#include
+
enum class RecordType
{
LaunchApplication,
+ LaunchRemoteApplication,
AttachToProcess,
ProfileSystem,
NUM_RECORD_TYPES
@@ -59,6 +63,12 @@ class RecordHost : public QObject
}
void setClientApplication(const QString& clientApplication);
+ QStringList clientApplicationArguments() const
+ {
+ return m_clientApplicationArguments;
+ }
+ void setClientApplicationArguments(const QStringList& arguments);
+
QString outputFileName() const
{
return m_outputFileName;
@@ -96,6 +106,15 @@ class RecordHost : public QObject
// list of pids to record
void setPids(const QStringList& pids);
+ bool isLocal() const;
+
+ const RemoteDevice* remoteDevice() const
+ {
+ return m_remoteDevice.get();
+ }
+
+ void disconnectFromDevice();
+
signals:
/// disallow "start" on recordpage until this is ready and that should only be the case when there's no error
void isReadyChanged(bool isReady);
@@ -104,6 +123,7 @@ class RecordHost : public QObject
void hostChanged();
void currentWorkingDirectoryChanged(const QString& cwd); // Maybe QUrl
void clientApplicationChanged(const QString& clientApplication);
+ void clientApplicationArgumentsChanged(const QStringList& arguments);
void perfCapabilitiesChanged(RecordHost::PerfCapabilities perfCapabilities);
void isPerfInstalledChanged(bool isInstalled);
void outputFileNameChanged(const QString& outputFileName);
@@ -111,12 +131,13 @@ class RecordHost : public QObject
void pidsChanged();
private:
- bool isLocal() const;
+ void checkRequirements();
QString m_host;
QString m_error;
QString m_cwd;
QString m_clientApplication;
+ QStringList m_clientApplicationArguments;
QString m_outputFileName;
PerfCapabilities m_perfCapabilities;
JobTracker m_checkPerfCapabilitiesJob;
@@ -124,6 +145,7 @@ class RecordHost : public QObject
RecordType m_recordType = RecordType::LaunchApplication;
bool m_isPerfInstalled = false;
QStringList m_pids;
+ std::unique_ptr m_remoteDevice;
};
Q_DECLARE_METATYPE(RecordHost::PerfCapabilities)
diff --git a/src/recordpage.cpp b/src/recordpage.cpp
index a6935e4f..6f919118 100644
--- a/src/recordpage.cpp
+++ b/src/recordpage.cpp
@@ -40,10 +40,10 @@
#include
#include
-#include "multiconfigwidget.h"
#include "perfoutputwidgetkonsole.h"
#include "perfoutputwidgettext.h"
#include "perfrecord.h"
+#include "settings.h"
namespace {
bool isIntel()
@@ -82,11 +82,15 @@ void updateStartRecordingButtonState(const RecordHost* host, const std::unique_p
return;
}
+ // TODO: move stuff to RecordHost
bool enabled = false;
switch (selectedRecordType(ui)) {
case RecordType::LaunchApplication:
enabled = ui->applicationName->url().isValid();
break;
+ case RecordType::LaunchRemoteApplication:
+ enabled = host->isReady();
+ break;
case RecordType::AttachToProcess:
enabled = ui->processesTableView->selectionModel()->hasSelection();
break;
@@ -192,13 +196,35 @@ RecordPage::RecordPage(QWidget* parent)
}
});
- connect(m_recordHost, &RecordHost::clientApplicationChanged, this, [this](const QString& filePath) {
- const auto config = applicationConfig(filePath);
- ui->workingDirectory->setText(config.readEntry("workingDir", QString()));
- ui->applicationParametersBox->setText(config.readEntry("params", QString()));
+ ui->multiConfig->setChildWidget(ui->launchWidget, {ui->applicationParametersBox, ui->workingDirectory});
+
+ connect(m_recordHost, &RecordHost::clientApplicationChanged, this,
+ [this](const QString& app) { ui->multiConfig->setConfigGroup(applicationConfig(app)); });
+
+ connect(ui->workingDirectory, qOverload(&KUrlRequester::returnPressed), this,
+ [this](const QString& cwd) {
+ ui->multiConfig->saveCurrentConfig();
+ m_recordHost->setCurrentWorkingDirectory(cwd);
+ });
+
+ connect(ui->applicationParametersBox, &QLineEdit::editingFinished, this, [this] {
+ ui->multiConfig->saveCurrentConfig();
+ m_recordHost->setClientApplicationArguments(KShell::splitArgs(ui->applicationParametersBox->text()));
+ });
- m_multiConfig->setConfig(applicationConfig(ui->applicationName->text()));
+ auto settings = Settings::instance();
+ connect(settings, &Settings::devicesChanged, this, [this](const QStringList& devices) {
+ ui->deviceComboBox->clear();
+ ui->deviceComboBox->insertItem(0, QStringLiteral("localhost"));
+ ui->deviceComboBox->insertItems(1, devices);
+ ui->deviceComboBox->setCurrentIndex(0);
});
+ ui->deviceComboBox->insertItem(0, QStringLiteral("localhost"));
+ ui->deviceComboBox->insertItems(1, settings->devices());
+ ui->deviceComboBox->setCurrentIndex(0);
+
+ connect(ui->deviceComboBox, qOverload(&QComboBox::currentIndexChanged), this,
+ [this] { m_recordHost->setHost(ui->deviceComboBox->currentText()); });
ui->compressionComboBox->addItem(tr("Disabled"), -1);
ui->compressionComboBox->addItem(tr("Enabled (Default Level)"), 0);
@@ -247,6 +273,7 @@ RecordPage::RecordPage(QWidget* parent)
});
m_recordHost->setHost(QStringLiteral("localhost"));
+ m_recordHost->setRecordType(RecordType::LaunchApplication);
ui->applicationName->comboBox()->setEditable(true);
ui->applicationName->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly);
@@ -271,30 +298,6 @@ RecordPage::RecordPage(QWidget* parent)
connect(m_perfOutput, &PerfOutputWidget::sendInput, this,
[this](const QByteArray& input) { m_perfRecord->sendInput(input); });
- auto saveFunction = [this](KConfigGroup group) {
- group.writeEntry("params", ui->applicationParametersBox->text());
- group.writeEntry("workingDir", ui->workingDirectory->text());
- };
-
- auto restoreFunction = [this](const KConfigGroup& group) {
- ui->applicationParametersBox->setText(group.readEntry("params", ""));
- ui->workingDirectory->setText(group.readEntry("workingDir", ""));
- setError({});
- };
-
- m_multiConfig = new MultiConfigWidget(this);
-
- connect(m_multiConfig, &MultiConfigWidget::saveConfig, this, saveFunction);
- connect(m_multiConfig, &MultiConfigWidget::restoreConfig, this, restoreFunction);
-
- m_multiConfig->setConfig(applicationConfig(ui->applicationName->text()));
-
- ui->launchAppBox->layout()->addWidget(m_multiConfig);
-
- connect(ui->applicationParametersBox, &QLineEdit::editingFinished, m_multiConfig,
- &MultiConfigWidget::updateCurrentConfig);
- connect(ui->workingDirectory, &KUrlRequester::textChanged, m_multiConfig, &MultiConfigWidget::updateCurrentConfig);
-
auto columnResizer = new KColumnResizer(this);
columnResizer->addWidgetsFromLayout(ui->formLayout);
columnResizer->addWidgetsFromLayout(ui->formLayout_1);
@@ -306,7 +309,11 @@ RecordPage::RecordPage(QWidget* parent)
connect(ui->startRecordingButton, &QPushButton::toggled, this, &RecordPage::onStartRecordingButtonClicked);
connect(ui->workingDirectory, &KUrlRequester::textChanged, m_recordHost,
&RecordHost::currentWorkingDirectoryChanged);
- connect(ui->viewPerfRecordResultsButton, &QPushButton::clicked, this, [this] { emit openFile(m_resultsFile); });
+ connect(ui->viewPerfRecordResultsButton, &QPushButton::clicked, this, [this] {
+ // we are done recording, disconnect from device
+ m_recordHost->disconnectFromDevice();
+ emit openFile(m_resultsFile);
+ });
connect(ui->outputFile, &KUrlRequester::textChanged, this, &RecordPage::onOutputFileNameChanged);
connect(ui->outputFile, static_cast(&KUrlRequester::returnPressed), this,
&RecordPage::onOutputFileNameSelected);
@@ -314,15 +321,18 @@ RecordPage::RecordPage(QWidget* parent)
ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-build")), tr("Launch Application"),
QVariant::fromValue(RecordType::LaunchApplication));
+ ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-build")), tr("Launch Remote Application"),
+ QVariant::fromValue(RecordType::LaunchRemoteApplication));
ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-install")), tr("Attach To Process(es)"),
QVariant::fromValue(RecordType::AttachToProcess));
ui->recordTypeComboBox->addItem(QIcon::fromTheme(QStringLiteral("run-build-install-root")), tr("Profile System"),
QVariant::fromValue(RecordType::ProfileSystem));
- connect(ui->recordTypeComboBox, qOverload(&QComboBox::currentIndexChanged), this,
- &RecordPage::updateRecordType);
+
connect(ui->recordTypeComboBox, qOverload(&QComboBox::currentIndexChanged), m_recordHost,
[this] { m_recordHost->setRecordType(ui->recordTypeComboBox->currentData().value()); });
- connect(m_recordHost, &RecordHost::clientApplicationChanged, this, &RecordPage::updateRecordType);
+
+ connect(m_recordHost, &RecordHost::recordTypeChanged, this, &RecordPage::updateRecordType);
+ updateRecordType(RecordType::LaunchApplication);
{
ui->callGraphComboBox->addItem(tr("None"), QVariant::fromValue(QString()));
@@ -528,7 +538,7 @@ void RecordPage::showRecordPage()
{
m_resultsFile.clear();
setError({});
- updateRecordType();
+ m_recordHost->setRecordType(RecordType::LaunchApplication);
ui->viewPerfRecordResultsButton->setEnabled(false);
}
@@ -645,14 +655,27 @@ void RecordPage::onStartRecordingButtonClicked(bool checked)
switch (recordType) {
case RecordType::LaunchApplication: {
const auto applicationName = m_recordHost->clientApplication();
- const auto appParameters = ui->applicationParametersBox->text();
+ const auto appParameters = m_recordHost->clientApplicationArguments();
auto workingDir = m_recordHost->currentWorkingDirectory();
if (workingDir.isEmpty()) {
workingDir = ui->workingDirectory->placeholderText();
}
- rememberApplication(applicationName, appParameters, workingDir, ui->applicationName->comboBox());
- m_perfRecord->record(perfOptions, outputFile, elevatePrivileges, applicationName,
- KShell::splitArgs(appParameters), workingDir);
+ rememberApplication(applicationName, appParameters.join(QLatin1Char(' ')), workingDir,
+ ui->applicationName->comboBox());
+ m_perfRecord->record(perfOptions, outputFile, elevatePrivileges);
+ break;
+ }
+ case RecordType::LaunchRemoteApplication: {
+ // TODO: network record
+ const auto applicationName = m_recordHost->clientApplication();
+ const auto appParameters = m_recordHost->clientApplicationArguments();
+ auto workingDir = m_recordHost->currentWorkingDirectory();
+ if (workingDir.isEmpty()) {
+ workingDir = ui->workingDirectory->placeholderText();
+ }
+ rememberApplication(applicationName, appParameters.join(QLatin1Char(' ')), workingDir,
+ ui->applicationName->comboBox());
+ m_perfRecord->record(perfOptions, outputFile, elevatePrivileges);
break;
}
case RecordType::AttachToProcess: {
@@ -752,17 +775,24 @@ void RecordPage::setError(const QString& message)
ui->applicationRecordErrorMessage->setVisible(!message.isEmpty());
}
-void RecordPage::updateRecordType()
+void RecordPage::updateRecordType(RecordType recordType)
{
setError({});
- const auto recordType = selectedRecordType(ui);
- ui->launchAppBox->setVisible(recordType == RecordType::LaunchApplication);
+ ui->launchAppBox->setVisible(recordType == RecordType::LaunchApplication
+ || recordType == RecordType::LaunchRemoteApplication);
ui->attachAppBox->setVisible(recordType == RecordType::AttachToProcess);
+ ui->remoteDeviceBox->setVisible(recordType == RecordType::LaunchRemoteApplication);
m_perfOutput->setInputVisible(recordType == RecordType::LaunchApplication);
m_perfOutput->clear();
+ if (recordType == RecordType::LaunchRemoteApplication) {
+ ui->applicationName->clear();
+ } else if (recordType == RecordType::LaunchApplication) {
+ restoreCombobox(config(), QStringLiteral("applications"), ui->applicationName->comboBox());
+ }
+
if (recordType == RecordType::AttachToProcess) {
updateProcesses();
}
diff --git a/src/recordpage.h b/src/recordpage.h
index ad2c09e8..0ed4fa41 100644
--- a/src/recordpage.h
+++ b/src/recordpage.h
@@ -27,7 +27,6 @@ class RecordPage;
class PerfRecord;
class ProcessModel;
class ProcessFilterModel;
-class MultiConfigWidget;
class PerfOutputWidget;
namespace KParts {
@@ -60,7 +59,7 @@ private slots:
private:
void recordingStopped();
- void updateRecordType();
+ void updateRecordType(RecordType type);
void appendOutput(const QString& text);
void setError(const QString& message);
@@ -73,7 +72,6 @@ private slots:
QTimer* m_updateRuntimeTimer;
KParts::ReadOnlyPart* m_konsolePart = nullptr;
QTemporaryFile* m_konsoleFile = nullptr;
- MultiConfigWidget* m_multiConfig;
PerfOutputWidget* m_perfOutput;
ProcessModel* m_processModel;
diff --git a/src/recordpage.ui b/src/recordpage.ui
index 0c53b30f..16a2b52e 100644
--- a/src/recordpage.ui
+++ b/src/recordpage.ui
@@ -50,78 +50,118 @@
-
-
+
- Launch Application
+ Remote Device
-
-
- QFormLayout::AllNonFixedFieldsGrow
-
+
-
-
-
- Path to the application to be recorded
-
+
- App&lication:
-
-
- applicationName
+ Device:
-
-
-
- Path to the application to be recorded
-
-
-
-
-
+
- -
-
-
- Optional parameters to pass to the application being recorded
-
-
- Parame&ters:
-
-
- applicationParametersBox
-
-
-
- -
-
-
- Qt::NoContextMenu
-
-
- Optional parameters to pass to the application being recorded
-
-
-
- -
-
-
- Directory to store the perf data file while recording
-
-
- Wor&king Directory:
-
-
- workingDirectory
-
-
+
+
+
+ -
+
+
+ Launch Application
+
+
+
-
+
- -
-
-
- Directory to store the perf data file while recording
-
+
-
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Path to the application to be recorded
+
+
+
+
+
+
+ -
+
+
+ Path to the application to be recorded
+
+
+ App&lication:
+
+
+ applicationName
+
+
+
+ -
+
+
+ Optional parameters to pass to the application being recorded
+
+
+ Parame&ters:
+
+
+ applicationParametersBox
+
+
+
+ -
+
+
+ Directory to store the perf data file while recording
+
+
+ Wor&king Directory:
+
+
+ workingDirectory
+
+
+
+ -
+
+
+ Directory to store the perf data file while recording
+
+
+
+ -
+
+
+ Qt::NoContextMenu
+
+
+ Optional parameters to pass to the application being recorded
+
+
+
+
@@ -620,6 +660,12 @@
1
+
+ MultiConfigWidget
+ QWidget
+
+ 1
+
diff --git a/src/remotedevice.cpp b/src/remotedevice.cpp
new file mode 100644
index 00000000..8cc89ecf
--- /dev/null
+++ b/src/remotedevice.cpp
@@ -0,0 +1,191 @@
+/*
+ SPDX-FileCopyrightText: Lieven Hey
+ SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "remotedevice.h"
+
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include "settings.h"
+
+namespace {
+QStringList sshArgs(const QString& dir)
+{
+ return {QStringLiteral("-o"), QStringLiteral("ControlMaster=auto"), QStringLiteral("-o"),
+ QStringLiteral("ControlPath=%1/ssh").arg(dir)};
+}
+
+void setupProcess(const std::unique_ptr& process, const QString& sshBinary, const KConfigGroup& config,
+ const QString& path, const QStringList& args)
+{
+ process->setProgram(sshBinary);
+ const auto options = config.readEntry("options", QString {});
+ const auto username = config.readEntry("username", QString {});
+ const auto hostname = config.readEntry("hostname", QString {});
+
+ auto arguments = sshArgs(path);
+ if (!options.isEmpty()) {
+ arguments.append(options.split(QLatin1Char(' ')));
+ }
+
+ arguments.append(QStringLiteral("%1@%2").arg(username, hostname));
+ if (!args.isEmpty()) {
+ arguments.append(args);
+ }
+ process->setArguments(arguments);
+}
+}
+
+RemoteDevice::RemoteDevice(QObject* parent)
+ : QObject(parent)
+{
+ Q_ASSERT(m_tempDir.isValid());
+ m_watcher.addPath(m_tempDir.path());
+
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this](const QString& path) {
+ // this could also be a delete so we need to check if the socket exists
+ if (QFile::exists(path + QStringLiteral("/ssh"))) {
+ // ssh only creates that file when the connection was established
+ emit connected();
+ }
+ });
+
+ auto findSSHBinary = [this](const QString& binary) {
+ if (binary.isEmpty()) {
+ m_sshBinary = QStandardPaths::findExecutable(QStringLiteral("ssh"));
+ } else {
+ m_sshBinary = binary;
+ }
+ };
+
+ auto settings = Settings::instance();
+ findSSHBinary(settings->sshPath());
+ connect(settings, &Settings::sshPathChanged, this, findSSHBinary);
+}
+
+RemoteDevice::~RemoteDevice()
+{
+ if (m_connection) {
+ disconnect();
+ QTimer::singleShot(0, this, [connection = m_connection.release()]() -> QCoro::Task {
+ co_await qCoro(connection).waitForFinished();
+ delete connection;
+ });
+ }
+}
+
+void RemoteDevice::connectToDevice(const QString& device)
+{
+ if (m_connection) {
+ disconnect();
+ }
+ auto config = KSharedConfig::openConfig()->group("SSH");
+ if (!config.hasGroup(device) && config.group(device).exists()) {
+ emit failedToConnect();
+ return;
+ }
+ m_config = config.group(device);
+
+ m_connection = sshProcess({});
+
+ connect(m_connection.get(), qOverload(&QProcess::finished), this,
+ [path = m_tempDir.path(), this](int exitCode, QProcess::ExitStatus) {
+ if (exitCode != 0) {
+ emit failedToConnect();
+ } else {
+ emit disconnected();
+ }
+ m_connection = nullptr;
+ });
+
+ m_connection->start();
+}
+
+void RemoteDevice::disconnect()
+{
+ if (m_connection) {
+ if (m_connection->state() == QProcess::ProcessState::Running) {
+ // ssh stops once you close the write channel
+ // we then use the finished signal for cleanup
+ m_connection->closeWriteChannel();
+ }
+ }
+}
+
+bool RemoteDevice::isConnected() const
+{
+ return QFile(m_tempDir.path() + QStringLiteral("/ssh")).exists();
+}
+
+bool RemoteDevice::checkIfProgramExists(const QString& program) const
+{
+ Q_ASSERT(QThread::currentThread() != thread());
+ // this is only used to check if perf is installed and is run in a background thread
+ auto ssh = sshProcess({QStringLiteral("command"), program});
+ ssh->start();
+ ssh->waitForFinished();
+ auto exitCode = ssh->exitCode();
+ // 128 -> not found
+ // perf will return 1 and display the help message
+ return exitCode != 128;
+}
+
+QCoro::Task RemoteDevice::checkIfDirectoryExists(const QString& directory) const
+{
+ auto _ssh = sshProcess({QStringLiteral("test"), QStringLiteral("-d"), directory});
+ auto ssh = qCoro(_ssh.get());
+ co_await ssh.start();
+ co_await ssh.waitForFinished();
+ auto exitCode = _ssh->exitCode();
+ co_return exitCode == 0;
+}
+
+QCoro::Task RemoteDevice::checkIfFileExists(const QString& file) const
+{
+ auto _ssh = sshProcess({QStringLiteral("test"), QStringLiteral("-f"), file});
+
+ auto ssh = qCoro(_ssh.get());
+ co_await ssh.start();
+ co_await ssh.waitForFinished();
+ auto exitCode = _ssh->exitCode();
+ co_return exitCode == 0;
+}
+
+QByteArray RemoteDevice::getProgramOutput(const QStringList& args) const
+{
+ auto ssh = sshProcess(args);
+ ssh->start();
+ ssh->waitForFinished();
+ auto output = ssh->readAllStandardOutput();
+ return output;
+}
+
+std::unique_ptr RemoteDevice::sshProcess(const QStringList& args) const
+{
+ if (!m_config.isValid()) {
+ return nullptr;
+ }
+
+ auto process = std::make_unique(nullptr);
+ setupProcess(process, m_sshBinary, m_config, m_tempDir.path(), args);
+
+ return process;
+}
+
+std::unique_ptr RemoteDevice::runPerf(const QString& cwd, const QStringList& perfOptions) const
+{
+ const auto perfCommand = QStringLiteral("perf record -o - %1 ").arg(perfOptions.join(QLatin1Char(' ')));
+ const QString command = QStringLiteral("cd %1 ; %2").arg(cwd, perfCommand);
+ auto process = sshProcess({QStringLiteral("sh"), QStringLiteral("-c"), QStringLiteral("\"%1\"").arg(command)});
+
+ return process;
+}
diff --git a/src/remotedevice.h b/src/remotedevice.h
new file mode 100644
index 00000000..5bc19a96
--- /dev/null
+++ b/src/remotedevice.h
@@ -0,0 +1,55 @@
+/*
+ SPDX-FileCopyrightText: Lieven Hey
+ SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#pragma once
+
+#include
+
+#include
+#include
+
+#include
+
+#include
+
+#include
+
+class QProcess;
+
+class RemoteDevice : public QObject
+{
+ Q_OBJECT
+public:
+ RemoteDevice(QObject* parent = nullptr);
+ ~RemoteDevice() override;
+
+ void connectToDevice(const QString& device);
+ void disconnect();
+
+ bool isConnected() const;
+
+ bool checkIfProgramExists(const QString& program) const;
+ QCoro::Task checkIfDirectoryExists(const QString& directory) const;
+ QCoro::Task checkIfFileExists(const QString& file) const;
+ QByteArray getProgramOutput(const QStringList& args) const;
+
+ std::unique_ptr runPerf(const QString& cdw, const QStringList& perfOptions) const;
+
+signals:
+ void connected();
+ void disconnected();
+ void failedToConnect();
+
+private:
+ std::unique_ptr sshProcess(const QStringList& args) const;
+
+ std::unique_ptr m_connection = nullptr;
+ QTemporaryDir m_tempDir;
+ QFileSystemWatcher m_watcher;
+ KConfigGroup m_config;
+ QString m_sshBinary;
+};
diff --git a/src/settings.cpp b/src/settings.cpp
index 03c33a9c..bed0bc97 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -243,6 +243,20 @@ void Settings::loadFromFile()
connect(this, &Settings::showBranchesChanged, [sharedConfig](bool showBranches) {
sharedConfig->group("Disassembly").writeEntry("showBranches", showBranches);
});
+
+ setSSHPath(sharedConfig->group("SSH").readEntry("ssh"));
+ setSSHCopyKeyPath(sharedConfig->group("SSH").readEntry("ssh-copy-id"));
+
+ const auto& ssh = sharedConfig->group("SSH");
+ setSSHPath(ssh.readEntry("ssh"));
+ setSSHCopyKeyPath(ssh.readEntry("ssh-copy-id"));
+
+ connect(this, &Settings::sshPathChanged, this,
+ [sharedConfig](const QString& path) { sharedConfig->group("SSH").writeEntry("ssh", path); });
+ connect(this, &Settings::sshCopyIdPathChanged, this,
+ [sharedConfig](const QString& path) { sharedConfig->group("SSH").writeEntry("ssh-copy-id", path); });
+
+ setDevices(ssh.groupList());
}
void Settings::setSourceCodePaths(const QString& paths)
@@ -268,3 +282,27 @@ void Settings::setShowBranches(bool showBranches)
emit showBranchesChanged(m_showBranches);
}
}
+
+void Settings::setSSHPath(const QString& sshPath)
+{
+ if (m_sshPath != sshPath) {
+ m_sshPath = sshPath;
+ emit sshPathChanged(m_sshPath);
+ }
+}
+
+void Settings::setSSHCopyKeyPath(const QString& sshCopyIdPath)
+{
+ if (m_sshCopyIdPath != sshCopyIdPath) {
+ m_sshCopyIdPath = sshCopyIdPath;
+ emit sshCopyIdPathChanged(m_sshCopyIdPath);
+ }
+}
+
+void Settings::setDevices(const QStringList& devices)
+{
+ if (m_devices != devices) {
+ m_devices = devices;
+ emit devicesChanged(m_devices);
+ }
+}
diff --git a/src/settings.h b/src/settings.h
index b038a5af..fd42be0a 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -159,6 +159,21 @@ class Settings : public QObject
return m_showBranches;
}
+ QString sshPath() const
+ {
+ return m_sshPath;
+ }
+
+ QString sshCopyIdPath() const
+ {
+ return m_sshCopyIdPath;
+ }
+
+ QStringList devices() const
+ {
+ return m_devices;
+ }
+
void loadFromFile();
signals:
@@ -182,6 +197,9 @@ class Settings : public QObject
void sourceCodePathsChanged(const QString& paths);
void perfPathChanged(const QString& perfPath);
void showBranchesChanged(bool showBranches);
+ void sshPathChanged(const QString& sshPath);
+ void sshCopyIdPathChanged(const QString& sshCopyIdPath);
+ void devicesChanged(const QStringList& devices);
public slots:
void setPrettifySymbols(bool prettifySymbols);
@@ -206,6 +224,9 @@ public slots:
void setSourceCodePaths(const QString& paths);
void setPerfPath(const QString& path);
void setShowBranches(bool showBranches);
+ void setSSHPath(const QString& path);
+ void setSSHCopyKeyPath(const QString& path);
+ void setDevices(const QStringList& devices);
private:
Settings() = default;
@@ -239,4 +260,8 @@ public slots:
QColor m_callgraphColor;
QString m_perfPath;
+ QString m_sshPath;
+ QString m_sshCopyIdPath;
+
+ QStringList m_devices;
};
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 92b1a73a..e257af40 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -13,6 +13,8 @@
#include "ui_disassemblysettingspage.h"
#include "ui_flamegraphsettingspage.h"
#include "ui_perfsettingspage.h"
+#include "ui_sourcepathsettings.h"
+#include "ui_sshsettingspage.h"
#include "ui_unwindsettingspage.h"
#include "multiconfigwidget.h"
@@ -26,15 +28,11 @@
#include
#include
#include
+#include
#include
namespace {
-KConfigGroup config()
-{
- return KSharedConfig::openConfig()->group("PerfPaths");
-}
-
QPushButton* setupMultiPath(KEditListWidget* listWidget, QLabel* buddy, QWidget* previous)
{
auto editor = new KUrlRequester(listWidget);
@@ -68,6 +66,7 @@ SettingsDialog::SettingsDialog(QWidget* parent)
#if KGraphViewerPart_FOUND
, callgraphPage(new Ui::CallgraphSettingsPage)
#endif
+ , sshSettingsPage(new Ui::SSHSettingsPage)
{
addPerfSettingsPage();
addPathSettingsPage();
@@ -77,6 +76,7 @@ SettingsDialog::SettingsDialog(QWidget* parent)
addCallgraphPage();
#endif
addSourcePathPage();
+ addSSHPage();
}
SettingsDialog::~SettingsDialog() = default;
@@ -85,44 +85,18 @@ void SettingsDialog::initSettings()
{
const auto configName = Settings::instance()->lastUsedEnvironment();
if (!configName.isEmpty()) {
- m_configs->selectConfig(configName);
+ unwindPage->multiConfig->loadConfig(configName);
}
}
-void SettingsDialog::initSettings(const QString& sysroot, const QString& appPath, const QString& extraLibPaths,
- const QString& debugPaths, const QString& kallsyms, const QString& arch,
- const QString& objdump)
-{
- auto fromPathString = [](KEditListWidget* listWidget, const QString& string) {
- listWidget->setItems(string.split(QLatin1Char(':'), Qt::SkipEmptyParts));
- };
- fromPathString(unwindPage->extraLibraryPaths, extraLibPaths);
- fromPathString(unwindPage->debugPaths, debugPaths);
-
- unwindPage->lineEditSysroot->setText(sysroot);
- unwindPage->lineEditApplicationPath->setText(appPath);
- unwindPage->lineEditKallsyms->setText(kallsyms);
- unwindPage->lineEditObjdump->setText(objdump);
-
- int itemIndex = 0;
- if (!arch.isEmpty()) {
- itemIndex = unwindPage->comboBoxArchitecture->findText(arch);
- if (itemIndex == -1) {
- itemIndex = unwindPage->comboBoxArchitecture->count();
- unwindPage->comboBoxArchitecture->addItem(arch);
- }
- }
- unwindPage->comboBoxArchitecture->setCurrentIndex(itemIndex);
-}
-
QString SettingsDialog::sysroot() const
{
- return unwindPage->lineEditSysroot->text();
+ return unwindPage->sysroot->text();
}
QString SettingsDialog::appPath() const
{
- return unwindPage->lineEditApplicationPath->text();
+ return unwindPage->appPath->text();
}
QString SettingsDialog::extraLibPaths() const
@@ -137,18 +111,18 @@ QString SettingsDialog::debugPaths() const
QString SettingsDialog::kallsyms() const
{
- return unwindPage->lineEditKallsyms->text();
+ return unwindPage->kallsyms->text();
}
QString SettingsDialog::arch() const
{
- const auto sArch = unwindPage->comboBoxArchitecture->currentText();
+ const auto sArch = unwindPage->arch->currentText();
return (sArch == QLatin1String("auto-detect")) ? QString() : sArch;
}
QString SettingsDialog::objdump() const
{
- return unwindPage->lineEditObjdump->text();
+ return unwindPage->objdump->text();
}
QString SettingsDialog::perfMapPath() const
@@ -178,61 +152,22 @@ void SettingsDialog::addPathSettingsPage()
auto item = addPage(page, tr("Unwinding"));
item->setHeader(tr("Unwind Options"));
item->setIcon(icon());
-
unwindPage->setupUi(page);
- auto lastExtraLibsWidget = setupMultiPath(unwindPage->extraLibraryPaths, unwindPage->extraLibraryPathsLabel,
- unwindPage->lineEditApplicationPath);
- setupMultiPath(unwindPage->debugPaths, unwindPage->debugPathsLabel, lastExtraLibsWidget);
-
- auto* label = new QLabel(this);
- label->setText(tr("Config:"));
-
- auto saveFunction = [this](KConfigGroup group) {
- group.writeEntry("sysroot", sysroot());
- group.writeEntry("appPath", appPath());
- group.writeEntry("extraLibPaths", extraLibPaths());
- group.writeEntry("debugPaths", debugPaths());
- group.writeEntry("kallsyms", kallsyms());
- group.writeEntry("arch", arch());
- group.writeEntry("objdump", objdump());
- };
-
- auto restoreFunction = [this](const KConfigGroup& group) {
- const auto sysroot = group.readEntry("sysroot");
- const auto appPath = group.readEntry("appPath");
- const auto extraLibPaths = group.readEntry("extraLibPaths");
- const auto debugPaths = group.readEntry("debugPaths");
- const auto kallsyms = group.readEntry("kallsyms");
- const auto arch = group.readEntry("arch");
- const auto objdump = group.readEntry("objdump");
- initSettings(sysroot, appPath, extraLibPaths, debugPaths, kallsyms, arch, objdump);
- ::config().writeEntry("lastUsed", m_configs->currentConfig());
- };
-
- m_configs = new MultiConfigWidget(this);
- m_configs->setConfig(config());
- m_configs->restoreCurrent();
-
- connect(m_configs, &MultiConfigWidget::saveConfig, this, saveFunction);
- connect(m_configs, &MultiConfigWidget::restoreConfig, this, restoreFunction);
-
- unwindPage->formLayout->insertRow(0, label, m_configs);
-
- connect(this, &KPageDialog::accepted, this, [this] { m_configs->updateCurrentConfig(); });
-
- for (auto field : {unwindPage->lineEditSysroot, unwindPage->lineEditApplicationPath, unwindPage->lineEditKallsyms,
- unwindPage->lineEditObjdump}) {
- connect(field, &KUrlRequester::textEdited, m_configs, &MultiConfigWidget::updateCurrentConfig);
- connect(field, &KUrlRequester::urlSelected, m_configs, &MultiConfigWidget::updateCurrentConfig);
- }
+ unwindPage->multiConfig->setConfigGroup(KSharedConfig::openConfig()->group("PerfPaths"));
+ unwindPage->multiConfig->setChildWidget(unwindPage->widget,
+ {unwindPage->sysroot, unwindPage->appPath, unwindPage->extraLibraryPaths,
+ unwindPage->debugPaths, unwindPage->kallsyms, unwindPage->arch,
+ unwindPage->objdump});
- connect(unwindPage->comboBoxArchitecture, QOverload::of(&QComboBox::currentIndexChanged), m_configs,
- &MultiConfigWidget::updateCurrentConfig);
+ auto lastExtraLibsWidget =
+ setupMultiPath(unwindPage->extraLibraryPaths, unwindPage->extraLibraryPathsLabel, unwindPage->appPath);
+ setupMultiPath(unwindPage->debugPaths, unwindPage->debugPathsLabel, lastExtraLibsWidget);
- connect(unwindPage->debugPaths, &KEditListWidget::changed, m_configs, &MultiConfigWidget::updateCurrentConfig);
- connect(unwindPage->extraLibraryPaths, &KEditListWidget::changed, m_configs,
- &MultiConfigWidget::updateCurrentConfig);
+ connect(buttonBox(), &QDialogButtonBox::accepted, this, [this] {
+ unwindPage->multiConfig->saveCurrentConfig();
+ Settings::instance()->setLastUsedEnvironment(unwindPage->multiConfig->currentConfig());
+ });
}
void SettingsDialog::keyPressEvent(QKeyEvent* event)
@@ -342,3 +277,74 @@ void SettingsDialog::addSourcePathPage()
settings->setShowBranches(disassemblyPage->showBranches->isChecked());
});
}
+
+void SettingsDialog::addSSHPage()
+{
+ auto page = new QWidget(this);
+ auto item = addPage(page, tr("SSH Settings"));
+ item->setHeader(tr("SSH Settings Page"));
+ item->setIcon(QIcon::fromTheme(QStringLiteral("preferences-system-windows-behavior")));
+ sshSettingsPage->setupUi(page);
+ sshSettingsPage->messageWidget->hide();
+ sshSettingsPage->errorWidget->hide();
+
+ bool ksshaskpassFound = !QStandardPaths()::findExe(QStringLiteral("ksshaskpass")).isEmpty();
+ if (!qEnvironmentVariableIsSet("SSH_ASKPASS") && !ksshaskpassFound) {
+ sshSettingsPage->errorWidget->setText(
+ tr("SSH_ASKPASS is not set please install ksshaskpass or set SSH_ASKPASS"));
+ sshSettingsPage->errorWidget->show();
+ }
+
+ auto configGroup = KSharedConfig::openConfig()->group("SSH");
+ sshSettingsPage->deviceConfig->setChildWidget(
+ sshSettingsPage->deviceSettings,
+ {sshSettingsPage->username, sshSettingsPage->hostname, sshSettingsPage->options});
+ sshSettingsPage->deviceConfig->setConfigGroup(configGroup);
+
+ connect(sshSettingsPage->copySshKeyButton, &QPushButton::pressed, this, [this] {
+ auto* copyKey = new QProcess(this);
+
+ auto path = sshSettingsPage->sshCopyIdPath->text();
+ if (path.isEmpty()) {
+ path = QStandardPaths::findExecutable(QStringLiteral("ssh-copy-id"));
+ }
+ if (path.isEmpty()) {
+ sshSettingsPage->messageWidget->setText(tr("Could not find ssh-copy-id"));
+ sshSettingsPage->messageWidget->show();
+ return;
+ }
+
+ copyKey->setProgram(path);
+
+ QStringList arguments = {};
+ auto options = sshSettingsPage->options->text();
+ if (!options.isEmpty()) {
+ arguments.append(options.split(QLatin1Char(' ')));
+ }
+ arguments.append(
+ QStringLiteral("%1@%2").arg(sshSettingsPage->username->text(), sshSettingsPage->hostname->text()));
+ copyKey->setArguments(arguments);
+
+ connect(copyKey, qOverload(&QProcess::finished), this,
+ [this, copyKey](int code, QProcess::ExitStatus) {
+ if (code == 0) {
+ sshSettingsPage->messageWidget->setText(QStringLiteral("Copy key successfully"));
+ sshSettingsPage->messageWidget->show();
+ } else {
+ sshSettingsPage->errorWidget->setText(QStringLiteral("Failed to copy key"));
+ sshSettingsPage->errorWidget->show();
+ }
+ copyKey->deleteLater();
+ });
+ copyKey->start();
+ });
+
+ connect(buttonBox(), &QDialogButtonBox::accepted, this, [this, configGroup] {
+ sshSettingsPage->deviceConfig->saveCurrentConfig();
+
+ auto settings = Settings::instance();
+ settings->setDevices(configGroup.groupList());
+ settings->setSSHPath(sshSettingsPage->sshBinary->text());
+ settings->setSSHCopyKeyPath(sshSettingsPage->sshCopyIdPath->text());
+ });
+}
diff --git a/src/settingsdialog.h b/src/settingsdialog.h
index 0b396d1a..1288093f 100644
--- a/src/settingsdialog.h
+++ b/src/settingsdialog.h
@@ -19,6 +19,7 @@ class DebuginfodPage;
class CallgraphSettingsPage;
class DisassemblySettingsPage;
class PerfSettingsPage;
+class SSHSettingsPage;
}
class MultiConfigWidget;
@@ -51,6 +52,7 @@ class SettingsDialog : public KPageDialog
void addDebuginfodPage();
void addCallgraphPage();
void addSourcePathPage();
+ void addSSHPage();
std::unique_ptr perfPage;
std::unique_ptr unwindPage;
@@ -58,5 +60,5 @@ class SettingsDialog : public KPageDialog
std::unique_ptr debuginfodPage;
std::unique_ptr disassemblyPage;
std::unique_ptr callgraphPage;
- MultiConfigWidget* m_configs;
+ std::unique_ptr sshSettingsPage;
};
diff --git a/src/sshsettingspage.ui b/src/sshsettingspage.ui
new file mode 100644
index 00000000..7151bfd8
--- /dev/null
+++ b/src/sshsettingspage.ui
@@ -0,0 +1,188 @@
+
+
+ SSHSettingsPage
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ -
+
+
+ KMessageWidget::Error
+
+
+
+ -
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Hostname:
+
+
+
+ -
+
+
+ -
+
+
+ Username:
+
+
+
+ -
+
+
+ -
+
+
+ Options:
+
+
+
+ -
+
+
+ -
+
+
+ Copy SSH Key
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ ssh:
+
+
+
+ -
+
+
+ ssh
+
+
+
+ -
+
+
+ ssh-copy-id:
+
+
+
+ -
+
+
+ ssh-copy-id
+
+
+
+
+
+
+
+
+
+
+ KUrlRequester
+ QWidget
+
+
+
+ KMessageWidget
+ QFrame
+
+ 1
+
+
+ MultiConfigWidget
+ QWidget
+
+ 1
+
+
+
+
+
diff --git a/src/unwindsettingspage.ui b/src/unwindsettingspage.ui
index 5c044135..066a7164 100644
--- a/src/unwindsettingspage.ui
+++ b/src/unwindsettingspage.ui
@@ -16,186 +16,210 @@
true
-
- -
-
-
- Path to the sysroot. Leave empty to use the local machine.
-
-
- local machine
-
-
-
- -
-
-
- Path to the application binary and library.
-
-
- Application Path:
-
-
- lineEditApplicationPath
-
-
-
- -
-
-
- Path to the application binary and library.
-
-
- auto-detect
-
-
-
- -
-
-
- Qt::TabFocus
-
-
- List of paths that contain additional libraries.
-
-
-
- -
-
-
- List of that contain debug information.
-
-
- Debug Paths:
-
-
-
- -
-
-
- Qt::TabFocus
-
-
- List of paths that contain debug information.
-
-
-
- -
-
-
- Path to the kernel symbol mapping.
-
-
- Kallsyms:
-
-
- lineEditKallsyms
-
-
-
- -
-
-
- Path to the kernel symbol mapping.
-
-
- auto-detect
-
-
-
-
-
-
- -
-
-
- System architecture, e.g. x86_64, arm, aarch64 etc.
-
-
- Architecture:
-
-
- comboBoxArchitecture
-
-
-
- -
-
-
- System architecture, e.g. x86_64, arm, aarch64 etc.
-
-
- true
-
-
-
-
- auto-detect
-
-
- -
-
- x64
-
-
- -
-
- ARMv7 (32 bit)
-
-
- -
-
- ARMv8 (64 bit)
-
-
-
-
- -
-
-
- <qt>The <tt>objdump</tt> binary that will be used for the disassembly view. Leave empty to let it be auto-detected.</qt>
-
-
- Objdump:
-
-
- lineEditObjdump
-
-
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
- -
-
-
- <qt>The <tt>objdump</tt> binary that will be used for the disassembly view. Leave empty to let it be auto-detected.</qt>
-
-
- auto-detect
-
-
-
-
-
-
- -
-
-
- List of paths that contain additional libraries.
-
-
- Extra Library Paths:
-
-
-
- -
-
-
- Path to the sysroot. Leave empty to use the local machine.
-
-
- Sysroot:
-
-
- lineEditSysroot
-
+
-
+
+
+
-
+
+
+ Path to the application binary and library.
+
+
+ auto-detect
+
+
+
+ -
+
+
+ Path to the sysroot. Leave empty to use the local machine.
+
+
+ local machine
+
+
+
+ -
+
+
+ Qt::TabFocus
+
+
+ List of paths that contain additional libraries.
+
+
+
+ -
+
+
+ List of paths that contain additional libraries.
+
+
+ Extra Library Paths:
+
+
+
+ -
+
+
+ Qt::TabFocus
+
+
+ List of paths that contain debug information.
+
+
+
+ -
+
+
+ Path to the kernel symbol mapping.
+
+
+ auto-detect
+
+
+
+
+
+
+ -
+
+
+ Path to the kernel symbol mapping.
+
+
+ Kallsyms:
+
+
+ kallsyms
+
+
+
+ -
+
+
+ List of that contain debug information.
+
+
+ Debug Paths:
+
+
+
+ -
+
+
+ Path to the application binary and library.
+
+
+ Application Path:
+
+
+ appPath
+
+
+
+ -
+
+
+ Path to the sysroot. Leave empty to use the local machine.
+
+
+ Sysroot:
+
+
+ sysroot
+
+
+
+ -
+
+
+ System architecture, e.g. x86_64, arm, aarch64 etc.
+
+
+ Architecture:
+
+
+ arch
+
+
+
+ -
+
+
+ System architecture, e.g. x86_64, arm, aarch64 etc.
+
+
+ true
+
+
-
+
+ auto-detect
+
+
+ -
+
+ x64
+
+
+ -
+
+ ARMv7 (32 bit)
+
+
+ -
+
+ ARMv8 (64 bit)
+
+
+
+
+ -
+
+
+ <qt>The <tt>objdump</tt> binary that will be used for the disassembly view. Leave empty to let it be auto-detected.</qt>
+
+
+ auto-detect
+
+
+
+
+
+
+ -
+
+
+ <qt>The <tt>objdump</tt> binary that will be used for the disassembly view. Leave empty to let it be auto-detected.</qt>
+
+
+ Objdump:
+
+
+ objdump
+
+
+
+
-
@@ -225,16 +249,13 @@
QWidget
+
+ MultiConfigWidget
+ QWidget
+
+ 1
+
-
- lineEditSysroot
- lineEditApplicationPath
- extraLibraryPaths
- debugPaths
- lineEditKallsyms
- comboBoxArchitecture
- lineEditObjdump
-
diff --git a/tests/integrationtests/CMakeLists.txt b/tests/integrationtests/CMakeLists.txt
index 48e751f9..06b51f86 100644
--- a/tests/integrationtests/CMakeLists.txt
+++ b/tests/integrationtests/CMakeLists.txt
@@ -7,6 +7,7 @@ ecm_add_test(
../../src/perfcontrolfifowrapper.cpp
../../src/perfrecord.cpp
../../src/recordhost.cpp
+ ../../src/remotedevice.cpp
../../src/settings.cpp
../../src/util.cpp
../../src/errnoutil.cpp
@@ -21,6 +22,7 @@ ecm_add_test(
KF${QT_MAJOR_VERSION}::WindowSystem
KF${QT_MAJOR_VERSION}::KIOCore
KF${QT_MAJOR_VERSION}::Parts
+ QCoro::Core
TEST_NAME
tst_perfparser
)
diff --git a/tests/integrationtests/tst_perfparser.cpp b/tests/integrationtests/tst_perfparser.cpp
index 184bff7e..43d14b51 100644
--- a/tests/integrationtests/tst_perfparser.cpp
+++ b/tests/integrationtests/tst_perfparser.cpp
@@ -19,8 +19,6 @@
#include "perfparser.h"
#include "perfrecord.h"
#include "recordhost.h"
-#include "unistd.h"
-#include "util.h"
#include "../testutils.h"
#include
@@ -429,7 +427,9 @@ private slots:
QSignalSpy recordingFinishedSpy(&perf, &PerfRecord::recordingFinished);
QSignalSpy recordingFailedSpy(&perf, &PerfRecord::recordingFailed);
- perf.record({QStringLiteral("--no-buildid-cache")}, tempFile.fileName(), false, exePath, exeOptions);
+ host.setClientApplication(exePath);
+ host.setClientApplicationArguments(exeOptions);
+ perf.record({QStringLiteral("--no-buildid-cache")}, tempFile.fileName(), false);
perf.sendInput(QByteArrayLiteral("some input\n"));
QVERIFY(recordingFinishedSpy.wait());
@@ -735,11 +735,13 @@ private slots:
QSignalSpy recordingFinishedSpy(&perf, &PerfRecord::recordingFinished);
QSignalSpy recordingFailedSpy(&perf, &PerfRecord::recordingFailed);
+ host.setClientApplication(exePath);
+ host.setClientApplicationArguments(exeOptions);
// always add `-c 1000000`, as perf's frequency mode is too unreliable for testing purposes
perf.record(
perfOptions
+ QStringList {QStringLiteral("-c"), QStringLiteral("1000000"), QStringLiteral("--no-buildid-cache")},
- fileName, false, exePath, exeOptions);
+ fileName, false);
VERIFY_OR_THROW(recordingFinishedSpy.wait(10000));