Skip to content

Commit ed17d32

Browse files
committed
add option to record on remote devices via ssh
most stuff is hardcoded but that will change in the next commit
1 parent 6c914c9 commit ed17d32

File tree

9 files changed

+558
-98
lines changed

9 files changed

+558
-98
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(hotspot_SRCS
2626

2727
parsers/perf/perfparser.cpp
2828
perfrecord.cpp
29+
perfrecordssh.cpp
2930

3031
mainwindow.cpp
3132
flamegraph.cpp

src/perfrecord.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ QString PerfRecord::sudoUtil()
369369
return {};
370370
}
371371

372+
bool PerfRecord::canElevatePrivileges()
373+
{
374+
return sudoUtil().isEmpty() && !KF5Auth_FOUND;
375+
}
376+
372377
QString PerfRecord::currentUsername()
373378
{
374379
return KUser().loginName();

src/perfrecord.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@ class PerfRecord : public QObject
2020
explicit PerfRecord(QObject* parent = nullptr);
2121
~PerfRecord();
2222

23-
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
24-
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory = QString());
25-
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
26-
const QStringList& pids);
27-
void recordSystem(const QStringList& perfOptions, const QString& outputPath);
23+
virtual void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
24+
const QString& exePath, const QStringList& exeOptions,
25+
const QString& workingDirectory = QString());
26+
virtual void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
27+
const QStringList& pids);
28+
virtual void recordSystem(const QStringList& perfOptions, const QString& outputPath);
2829

2930
const QString perfCommand();
30-
void stopRecording();
31-
void sendInput(const QByteArray& input);
31+
virtual void stopRecording();
32+
virtual void sendInput(const QByteArray& input);
3233

3334
virtual QString sudoUtil();
35+
virtual bool canElevatePrivileges();
3436
virtual QString currentUsername();
3537

3638
virtual bool canTrace(const QString& path);

src/perfrecordssh.cpp

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
SPDX-FileCopyrightText: Lieven Hey <lieven.hey@kdab.com>
3+
SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
4+
5+
SPDX-License-Identifier: GPL-2.0-or-later
6+
*/
7+
8+
#include "perfrecordssh.h"
9+
10+
#include <QDir>
11+
#include <QFile>
12+
#include <QFileInfo>
13+
#include <QProcess>
14+
#include <QStandardPaths>
15+
16+
#include <csignal>
17+
18+
#include "hotspot-config.h"
19+
20+
QString sshOutput(const QString& hostname, const QStringList& command)
21+
{
22+
QProcess ssh;
23+
ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
24+
const auto arguments = QStringList({hostname}) + command;
25+
ssh.setArguments(arguments);
26+
ssh.start();
27+
ssh.waitForFinished();
28+
return QString::fromUtf8(ssh.readAll());
29+
}
30+
31+
int sshExitCode(const QString& hostname, const QStringList& command)
32+
{
33+
QProcess ssh;
34+
ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
35+
const auto arguments = QStringList({hostname}) + command;
36+
ssh.setArguments(arguments);
37+
ssh.start();
38+
ssh.waitForFinished();
39+
return ssh.exitCode();
40+
}
41+
42+
PerfRecordSSH::PerfRecordSSH(QObject* parent)
43+
: PerfRecord(parent)
44+
{
45+
m_hostname = QStringLiteral("user@localhost");
46+
}
47+
48+
PerfRecordSSH::~PerfRecordSSH() = default;
49+
50+
void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,
51+
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory)
52+
{
53+
int exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-e"), exePath});
54+
if (exitCode) {
55+
emit recordingFailed(tr("File '%1' does not exist.").arg(exePath));
56+
}
57+
58+
exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-f"), exePath});
59+
if (exitCode) {
60+
emit recordingFailed(tr("'%1' is not a file.").arg(exePath));
61+
}
62+
63+
exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-x"), exePath});
64+
if (exitCode) {
65+
emit recordingFailed(tr("File '%1' is not executable.").arg(exePath));
66+
}
67+
68+
QStringList recordOptions = {exePath};
69+
recordOptions += exeOptions;
70+
71+
startRecording(perfOptions, outputPath, recordOptions, workingDirectory);
72+
}
73+
74+
void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,
75+
const QStringList& pids)
76+
{
77+
if (pids.empty()) {
78+
emit recordingFailed(tr("Process does not exist."));
79+
return;
80+
}
81+
82+
QStringList options = perfOptions;
83+
options += {QStringLiteral("--pid"), pids.join(QLatin1Char(','))};
84+
startRecording(options, outputPath, {}, {});
85+
}
86+
87+
void PerfRecordSSH::recordSystem(const QStringList& perfOptions, const QString& outputPath)
88+
{
89+
auto options = perfOptions;
90+
options.append(QStringLiteral("--all-cpus"));
91+
startRecording(options, outputPath, {}, {});
92+
}
93+
94+
void PerfRecordSSH::stopRecording()
95+
{
96+
if (m_recordProcess) {
97+
m_userTerminated = true;
98+
m_outputFile->close();
99+
m_recordProcess->terminate();
100+
m_recordProcess->waitForFinished();
101+
m_recordProcess = nullptr;
102+
}
103+
}
104+
105+
void PerfRecordSSH::sendInput(const QByteArray& input)
106+
{
107+
if (m_recordProcess)
108+
m_recordProcess->write(input);
109+
}
110+
111+
QString PerfRecordSSH::currentUsername()
112+
{
113+
if (m_hostname.isEmpty())
114+
return {};
115+
return sshOutput(m_hostname, {QLatin1String("echo"), QLatin1String("$USERNAME")}).simplified();
116+
}
117+
118+
bool PerfRecordSSH::canTrace(const QString& path)
119+
{
120+
if (m_hostname.isEmpty())
121+
return false;
122+
123+
// exit code == 0 -> true
124+
bool isDir = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-d"), path}) == 0;
125+
bool isReadable = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-r"), path}) == 0;
126+
127+
if (!isDir || !isReadable) {
128+
return false;
129+
}
130+
131+
QString paranoid =
132+
sshOutput(m_hostname, {QLatin1String("cat"), QLatin1String("/proc/sys/kernel/perf_event_paranoid")});
133+
return paranoid.trimmed() == QLatin1String("-1");
134+
}
135+
136+
bool PerfRecordSSH::canProfileOffCpu()
137+
{
138+
if (m_hostname.isEmpty())
139+
return false;
140+
return canTrace(QStringLiteral("events/sched/sched_switch"));
141+
}
142+
143+
static QString perfRecordHelp(const QString& hostname)
144+
{
145+
static const QString recordHelp = [hostname]() {
146+
static QString help =
147+
sshOutput(hostname, {QLatin1String("perf"), QLatin1String("record"), QLatin1String("--help")});
148+
if (help.isEmpty()) {
149+
// no man page installed, assume the best
150+
help = QLatin1String("--sample-cpu --switch-events");
151+
}
152+
return help;
153+
}();
154+
return recordHelp;
155+
}
156+
157+
static QString perfBuildOptions(const QString& hostname)
158+
{
159+
static const QString buildOptionsHelper = [hostname]() {
160+
static QString buildOptions =
161+
sshOutput(hostname, {QLatin1String("perf"), QLatin1String("version"), QLatin1String("--build-options")});
162+
return buildOptions;
163+
}();
164+
return buildOptionsHelper;
165+
}
166+
167+
bool PerfRecordSSH::canSampleCpu()
168+
{
169+
if (m_hostname.isEmpty())
170+
return false;
171+
return perfRecordHelp(m_hostname).contains(QLatin1String("--sample-cpu"));
172+
}
173+
174+
bool PerfRecordSSH::canSwitchEvents()
175+
{
176+
if (m_hostname.isEmpty())
177+
return false;
178+
return perfRecordHelp(m_hostname).contains(QLatin1String("--switch-events"));
179+
}
180+
181+
bool PerfRecordSSH::canUseAio()
182+
{
183+
// somehow this doesn't work with ssh
184+
return false;
185+
}
186+
187+
bool PerfRecordSSH::canCompress()
188+
{
189+
if (m_hostname.isEmpty())
190+
return false;
191+
return Zstd_FOUND && perfBuildOptions(m_hostname).contains(QLatin1String("zstd: [ on ]"));
192+
}
193+
194+
bool PerfRecordSSH::isPerfInstalled()
195+
{
196+
if (m_hostname.isEmpty())
197+
return false;
198+
return sshExitCode(m_hostname, {QLatin1String("perf")}) != 127;
199+
}
200+
201+
void PerfRecordSSH::startRecording(const QStringList& perfOptions, const QString& outputPath,
202+
const QStringList& recordOptions, const QString& workingDirectory)
203+
{
204+
if (m_recordProcess) {
205+
stopRecording();
206+
}
207+
208+
QFileInfo outputFileInfo(outputPath);
209+
QString folderPath = outputFileInfo.dir().path();
210+
QFileInfo folderInfo(folderPath);
211+
if (!folderInfo.exists()) {
212+
emit recordingFailed(tr("Folder '%1' does not exist.").arg(folderPath));
213+
return;
214+
}
215+
if (!folderInfo.isDir()) {
216+
emit recordingFailed(tr("'%1' is not a folder.").arg(folderPath));
217+
return;
218+
}
219+
if (!folderInfo.isWritable()) {
220+
emit recordingFailed(tr("Folder '%1' is not writable.").arg(folderPath));
221+
return;
222+
}
223+
224+
QStringList perfCommand = {QStringLiteral("record"), QStringLiteral("-o"), QStringLiteral("-")};
225+
perfCommand += perfOptions;
226+
perfCommand += recordOptions;
227+
228+
m_outputFile = new QFile(outputPath);
229+
m_outputFile->open(QIODevice::WriteOnly);
230+
231+
m_recordProcess = new QProcess(this);
232+
m_recordProcess->setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));
233+
m_recordProcess->setArguments({m_hostname, QLatin1String("perf ") + perfCommand.join(QLatin1Char(' '))});
234+
m_recordProcess->start();
235+
m_recordProcess->waitForStarted();
236+
237+
qDebug() << m_recordProcess->arguments().join(QLatin1Char(' '));
238+
239+
emit recordingStarted(QLatin1String("perf"), perfCommand);
240+
241+
connect(m_recordProcess, &QProcess::readyReadStandardOutput, this,
242+
[this] { m_outputFile->write(m_recordProcess->readAllStandardOutput()); });
243+
244+
connect(m_recordProcess, &QProcess::readyReadStandardError, this,
245+
[this] { emit recordingOutput(QString::fromUtf8(m_recordProcess->readAllStandardError())); });
246+
247+
connect(m_recordProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,
248+
[this](int exitCode, QProcess::ExitStatus exitStatus) {
249+
Q_UNUSED(exitStatus)
250+
251+
m_outputFile->close();
252+
253+
if ((exitCode == EXIT_SUCCESS || (exitCode == SIGTERM && m_userTerminated) || m_outputFile->size() > 0)
254+
&& m_outputFile->exists()) {
255+
if (exitCode != EXIT_SUCCESS && !m_userTerminated) {
256+
emit debuggeeCrashed();
257+
}
258+
emit recordingFinished(m_outputFile->fileName());
259+
} else {
260+
emit recordingFailed(tr("Failed to record perf data, error code %1.").arg(exitCode));
261+
}
262+
m_userTerminated = false;
263+
264+
emit recordingFinished(m_outputFile->fileName());
265+
});
266+
}

src/perfrecordssh.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
SPDX-FileCopyrightText: Lieven Hey <lieven.hey@kdab.com>
3+
SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
4+
5+
SPDX-License-Identifier: GPL-2.0-or-later
6+
*/
7+
8+
#pragma once
9+
10+
#include "perfrecord.h"
11+
12+
class QFile;
13+
14+
class PerfRecordSSH : public PerfRecord
15+
{
16+
Q_OBJECT
17+
public:
18+
PerfRecordSSH(QObject* parent = nullptr);
19+
~PerfRecordSSH();
20+
21+
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
22+
const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory) override;
23+
void record(const QStringList& perfOptions, const QString& outputPath, bool elevatePrivileges,
24+
const QStringList& pids) override;
25+
void recordSystem(const QStringList& perfOptions, const QString& outputPath) override;
26+
void stopRecording() override;
27+
void sendInput(const QByteArray& input) override;
28+
29+
QString sudoUtil() override
30+
{
31+
return {};
32+
};
33+
bool canElevatePrivileges() override
34+
{
35+
return false;
36+
}
37+
QString currentUsername() override;
38+
39+
bool canTrace(const QString& path) override;
40+
bool canProfileOffCpu() override;
41+
bool canSampleCpu() override;
42+
bool canSwitchEvents() override;
43+
bool canUseAio() override;
44+
bool canCompress() override;
45+
bool isPerfInstalled() override;
46+
47+
private:
48+
void startRecording(const QStringList& perfOptions, const QString& outputPath, const QStringList& recordOptions,
49+
const QString& workingDirectory);
50+
51+
QProcess* m_recordProcess = nullptr;
52+
QFile* m_outputFile;
53+
QString m_hostname;
54+
bool m_userTerminated = false;
55+
};

0 commit comments

Comments
 (0)