| 
 | 1 | +/*  | 
 | 2 | +  perfrecordssh.cpp  | 
 | 3 | +
  | 
 | 4 | +  This file is part of Hotspot, the Qt GUI for performance analysis.  | 
 | 5 | +
  | 
 | 6 | +  Copyright (C) 2021 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com  | 
 | 7 | +  Author: Lieven Hey <lieven.hey@kdab.com>  | 
 | 8 | +
  | 
 | 9 | +  Licensees holding valid commercial KDAB Hotspot licenses may use this file in  | 
 | 10 | +  accordance with Hotspot Commercial License Agreement provided with the Software.  | 
 | 11 | +
  | 
 | 12 | +  Contact info@kdab.com if any conditions of this licensing are not clear to you.  | 
 | 13 | +
  | 
 | 14 | +  This program is free software; you can redistribute it and/or modify  | 
 | 15 | +  it under the terms of the GNU General Public License as published by  | 
 | 16 | +  the Free Software Foundation, either version 2 of the License, or  | 
 | 17 | +  (at your option) any later version.  | 
 | 18 | +
  | 
 | 19 | +  This program is distributed in the hope that it will be useful,  | 
 | 20 | +  but WITHOUT ANY WARRANTY; without even the implied warranty of  | 
 | 21 | +  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  | 
 | 22 | +  GNU General Public License for more details.  | 
 | 23 | +
  | 
 | 24 | +  You should have received a copy of the GNU General Public License  | 
 | 25 | +  along with this program.  If not, see <http://www.gnu.org/licenses/>.  | 
 | 26 | +*/  | 
 | 27 | + | 
 | 28 | +#include "perfrecordssh.h"  | 
 | 29 | + | 
 | 30 | +#include <QDir>  | 
 | 31 | +#include <QFile>  | 
 | 32 | +#include <QFileInfo>  | 
 | 33 | +#include <QProcess>  | 
 | 34 | +#include <QStandardPaths>  | 
 | 35 | + | 
 | 36 | +#include <csignal>  | 
 | 37 | + | 
 | 38 | +#include "hotspot-config.h"  | 
 | 39 | + | 
 | 40 | +QString sshOutput(const QString& hostname, const QStringList& command)  | 
 | 41 | +{  | 
 | 42 | +    QProcess ssh;  | 
 | 43 | +    ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));  | 
 | 44 | +    const auto arguments = QStringList({hostname}) + command;  | 
 | 45 | +    ssh.setArguments(arguments);  | 
 | 46 | +    ssh.start();  | 
 | 47 | +    ssh.waitForFinished();  | 
 | 48 | +    return QString::fromUtf8(ssh.readAll());  | 
 | 49 | +}  | 
 | 50 | + | 
 | 51 | +int sshExitCode(const QString& hostname, const QStringList& command)  | 
 | 52 | +{  | 
 | 53 | +    QProcess ssh;  | 
 | 54 | +    ssh.setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));  | 
 | 55 | +    const auto arguments = QStringList({hostname}) + command;  | 
 | 56 | +    ssh.setArguments(arguments);  | 
 | 57 | +    ssh.start();  | 
 | 58 | +    ssh.waitForFinished();  | 
 | 59 | +    return ssh.exitCode();  | 
 | 60 | +}  | 
 | 61 | + | 
 | 62 | +PerfRecordSSH::PerfRecordSSH(QObject* parent)  | 
 | 63 | +    : PerfRecord(parent)  | 
 | 64 | +{  | 
 | 65 | +    m_hostname = QStringLiteral("user@localhost");  | 
 | 66 | +}  | 
 | 67 | + | 
 | 68 | +PerfRecordSSH::~PerfRecordSSH() = default;  | 
 | 69 | + | 
 | 70 | +void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,  | 
 | 71 | +                           const QString& exePath, const QStringList& exeOptions, const QString& workingDirectory)  | 
 | 72 | +{  | 
 | 73 | +    int exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-e"), exePath});  | 
 | 74 | +    if (exitCode) {  | 
 | 75 | +        emit recordingFailed(tr("File '%1' does not exist.").arg(exePath));  | 
 | 76 | +    }  | 
 | 77 | + | 
 | 78 | +    exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-f"), exePath});  | 
 | 79 | +    if (exitCode) {  | 
 | 80 | +        emit recordingFailed(tr("'%1' is not a file.").arg(exePath));  | 
 | 81 | +    }  | 
 | 82 | + | 
 | 83 | +    exitCode = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-x"), exePath});  | 
 | 84 | +    if (exitCode) {  | 
 | 85 | +        emit recordingFailed(tr("File '%1' is not executable.").arg(exePath));  | 
 | 86 | +    }  | 
 | 87 | + | 
 | 88 | +    QStringList recordOptions = {exePath};  | 
 | 89 | +    recordOptions += exeOptions;  | 
 | 90 | + | 
 | 91 | +    startRecording(perfOptions, outputPath, recordOptions, workingDirectory);  | 
 | 92 | +}  | 
 | 93 | + | 
 | 94 | +void PerfRecordSSH::record(const QStringList& perfOptions, const QString& outputPath, bool /*elevatePrivileges*/,  | 
 | 95 | +                           const QStringList& pids)  | 
 | 96 | +{  | 
 | 97 | +    if (pids.empty()) {  | 
 | 98 | +        emit recordingFailed(tr("Process does not exist."));  | 
 | 99 | +        return;  | 
 | 100 | +    }  | 
 | 101 | + | 
 | 102 | +    QStringList options = perfOptions;  | 
 | 103 | +    options += {QStringLiteral("--pid"), pids.join(QLatin1Char(','))};  | 
 | 104 | +    startRecording(options, outputPath, {}, {});  | 
 | 105 | +}  | 
 | 106 | + | 
 | 107 | +void PerfRecordSSH::recordSystem(const QStringList& perfOptions, const QString& outputPath)  | 
 | 108 | +{  | 
 | 109 | +    auto options = perfOptions;  | 
 | 110 | +    options.append(QStringLiteral("--all-cpus"));  | 
 | 111 | +    startRecording(options, outputPath, {}, {});  | 
 | 112 | +}  | 
 | 113 | + | 
 | 114 | +void PerfRecordSSH::stopRecording()  | 
 | 115 | +{  | 
 | 116 | +    if (m_recordProcess) {  | 
 | 117 | +        m_userTerminated = true;  | 
 | 118 | +        m_outputFile->close();  | 
 | 119 | +        m_recordProcess->terminate();  | 
 | 120 | +        m_recordProcess->waitForFinished();  | 
 | 121 | +        m_recordProcess = nullptr;  | 
 | 122 | +    }  | 
 | 123 | +}  | 
 | 124 | + | 
 | 125 | +void PerfRecordSSH::sendInput(const QByteArray& input)  | 
 | 126 | +{  | 
 | 127 | +    if (m_recordProcess)  | 
 | 128 | +        m_recordProcess->write(input);  | 
 | 129 | +}  | 
 | 130 | + | 
 | 131 | +QString PerfRecordSSH::currentUsername()  | 
 | 132 | +{  | 
 | 133 | +    if (m_hostname.isEmpty())  | 
 | 134 | +        return {};  | 
 | 135 | +    return sshOutput(m_hostname, {QLatin1String("echo"), QLatin1String("$USERNAME")}).simplified();  | 
 | 136 | +}  | 
 | 137 | + | 
 | 138 | +bool PerfRecordSSH::canTrace(const QString& path)  | 
 | 139 | +{  | 
 | 140 | +    if (m_hostname.isEmpty())  | 
 | 141 | +        return false;  | 
 | 142 | + | 
 | 143 | +    // exit code == 0 -> true  | 
 | 144 | +    bool isDir = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-d"), path}) == 0;  | 
 | 145 | +    bool isReadable = sshExitCode(m_hostname, {QLatin1String("test"), QLatin1String("-r"), path}) == 0;  | 
 | 146 | + | 
 | 147 | +    if (!isDir || !isReadable) {  | 
 | 148 | +        return false;  | 
 | 149 | +    }  | 
 | 150 | + | 
 | 151 | +    QString paranoid =  | 
 | 152 | +        sshOutput(m_hostname, {QLatin1String("cat"), QLatin1String("/proc/sys/kernel/perf_event_paranoid")});  | 
 | 153 | +    return paranoid.trimmed() == QLatin1String("-1");  | 
 | 154 | +}  | 
 | 155 | + | 
 | 156 | +bool PerfRecordSSH::canProfileOffCpu()  | 
 | 157 | +{  | 
 | 158 | +    if (m_hostname.isEmpty())  | 
 | 159 | +        return false;  | 
 | 160 | +    return canTrace(QStringLiteral("events/sched/sched_switch"));  | 
 | 161 | +}  | 
 | 162 | + | 
 | 163 | +static QString perfRecordHelp(const QString& hostname)  | 
 | 164 | +{  | 
 | 165 | +    static const QString recordHelp = [hostname]() {  | 
 | 166 | +        static QString help =  | 
 | 167 | +            sshOutput(hostname, {QLatin1String("perf"), QLatin1String("record"), QLatin1String("--help")});  | 
 | 168 | +        if (help.isEmpty()) {  | 
 | 169 | +            // no man page installed, assume the best  | 
 | 170 | +            help = QLatin1String("--sample-cpu --switch-events");  | 
 | 171 | +        }  | 
 | 172 | +        return help;  | 
 | 173 | +    }();  | 
 | 174 | +    return recordHelp;  | 
 | 175 | +}  | 
 | 176 | + | 
 | 177 | +static QString perfBuildOptions(const QString& hostname)  | 
 | 178 | +{  | 
 | 179 | +    static const QString buildOptionsHelper = [hostname]() {  | 
 | 180 | +        static QString buildOptions =  | 
 | 181 | +            sshOutput(hostname, {QLatin1String("perf"), QLatin1String("version"), QLatin1String("--build-options")});  | 
 | 182 | +        return buildOptions;  | 
 | 183 | +    }();  | 
 | 184 | +    return buildOptionsHelper;  | 
 | 185 | +}  | 
 | 186 | + | 
 | 187 | +bool PerfRecordSSH::canSampleCpu()  | 
 | 188 | +{  | 
 | 189 | +    if (m_hostname.isEmpty())  | 
 | 190 | +        return false;  | 
 | 191 | +    return perfRecordHelp(m_hostname).contains(QLatin1String("--sample-cpu"));  | 
 | 192 | +}  | 
 | 193 | + | 
 | 194 | +bool PerfRecordSSH::canSwitchEvents()  | 
 | 195 | +{  | 
 | 196 | +    if (m_hostname.isEmpty())  | 
 | 197 | +        return false;  | 
 | 198 | +    return perfRecordHelp(m_hostname).contains(QLatin1String("--switch-events"));  | 
 | 199 | +}  | 
 | 200 | + | 
 | 201 | +bool PerfRecordSSH::canUseAio()  | 
 | 202 | +{  | 
 | 203 | +    // somehow this doesn't work with ssh  | 
 | 204 | +    return false;  | 
 | 205 | +}  | 
 | 206 | + | 
 | 207 | +bool PerfRecordSSH::canCompress()  | 
 | 208 | +{  | 
 | 209 | +    if (m_hostname.isEmpty())  | 
 | 210 | +        return false;  | 
 | 211 | +    return Zstd_FOUND && perfBuildOptions(m_hostname).contains(QLatin1String("zstd: [ on  ]"));  | 
 | 212 | +}  | 
 | 213 | + | 
 | 214 | +bool PerfRecordSSH::isPerfInstalled()  | 
 | 215 | +{  | 
 | 216 | +    if (m_hostname.isEmpty())  | 
 | 217 | +        return false;  | 
 | 218 | +    return sshExitCode(m_hostname, {QLatin1String("perf")}) != 127;  | 
 | 219 | +}  | 
 | 220 | + | 
 | 221 | +void PerfRecordSSH::startRecording(const QStringList& perfOptions, const QString& outputPath,  | 
 | 222 | +                                   const QStringList& recordOptions, const QString& workingDirectory)  | 
 | 223 | +{  | 
 | 224 | +    if (m_recordProcess) {  | 
 | 225 | +        stopRecording();  | 
 | 226 | +    }  | 
 | 227 | + | 
 | 228 | +    QFileInfo outputFileInfo(outputPath);  | 
 | 229 | +    QString folderPath = outputFileInfo.dir().path();  | 
 | 230 | +    QFileInfo folderInfo(folderPath);  | 
 | 231 | +    if (!folderInfo.exists()) {  | 
 | 232 | +        emit recordingFailed(tr("Folder '%1' does not exist.").arg(folderPath));  | 
 | 233 | +        return;  | 
 | 234 | +    }  | 
 | 235 | +    if (!folderInfo.isDir()) {  | 
 | 236 | +        emit recordingFailed(tr("'%1' is not a folder.").arg(folderPath));  | 
 | 237 | +        return;  | 
 | 238 | +    }  | 
 | 239 | +    if (!folderInfo.isWritable()) {  | 
 | 240 | +        emit recordingFailed(tr("Folder '%1' is not writable.").arg(folderPath));  | 
 | 241 | +        return;  | 
 | 242 | +    }  | 
 | 243 | + | 
 | 244 | +    QStringList perfCommand = {QStringLiteral("record"), QStringLiteral("-o"), QStringLiteral("-")};  | 
 | 245 | +    perfCommand += perfOptions;  | 
 | 246 | +    perfCommand += recordOptions;  | 
 | 247 | + | 
 | 248 | +    m_outputFile = new QFile(outputPath);  | 
 | 249 | +    m_outputFile->open(QIODevice::WriteOnly);  | 
 | 250 | + | 
 | 251 | +    m_recordProcess = new QProcess(this);  | 
 | 252 | +    m_recordProcess->setProgram(QStandardPaths::findExecutable(QLatin1String("ssh")));  | 
 | 253 | +    m_recordProcess->setArguments({m_hostname, QLatin1String("perf ") + perfCommand.join(QLatin1Char(' '))});  | 
 | 254 | +    m_recordProcess->start();  | 
 | 255 | +    m_recordProcess->waitForStarted();  | 
 | 256 | + | 
 | 257 | +    qDebug() << m_recordProcess->arguments().join(QLatin1Char(' '));  | 
 | 258 | + | 
 | 259 | +    emit recordingStarted(QLatin1String("perf"), perfCommand);  | 
 | 260 | + | 
 | 261 | +    connect(m_recordProcess, &QProcess::readyReadStandardOutput, this,  | 
 | 262 | +            [this] { m_outputFile->write(m_recordProcess->readAllStandardOutput()); });  | 
 | 263 | + | 
 | 264 | +    connect(m_recordProcess, &QProcess::readyReadStandardError, this,  | 
 | 265 | +            [this] { emit recordingOutput(QString::fromUtf8(m_recordProcess->readAllStandardError())); });  | 
 | 266 | + | 
 | 267 | +    connect(m_recordProcess, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this,  | 
 | 268 | +            [this](int exitCode, QProcess::ExitStatus exitStatus) {  | 
 | 269 | +                Q_UNUSED(exitStatus)  | 
 | 270 | + | 
 | 271 | +                m_outputFile->close();  | 
 | 272 | + | 
 | 273 | +                if ((exitCode == EXIT_SUCCESS || (exitCode == SIGTERM && m_userTerminated) || m_outputFile->size() > 0)  | 
 | 274 | +                    && m_outputFile->exists()) {  | 
 | 275 | +                    if (exitCode != EXIT_SUCCESS && !m_userTerminated) {  | 
 | 276 | +                        emit debuggeeCrashed();  | 
 | 277 | +                    }  | 
 | 278 | +                    emit recordingFinished(m_outputFile->fileName());  | 
 | 279 | +                } else {  | 
 | 280 | +                    emit recordingFailed(tr("Failed to record perf data, error code %1.").arg(exitCode));  | 
 | 281 | +                }  | 
 | 282 | +                m_userTerminated = false;  | 
 | 283 | + | 
 | 284 | +                emit recordingFinished(m_outputFile->fileName());  | 
 | 285 | +            });  | 
 | 286 | +}  | 
0 commit comments