1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "callgrindcontroller.h"
27 
28 #include <ssh/sftpsession.h>
29 #include <ssh/sshconnectionmanager.h>
30 
31 #include <utils/fileutils.h>
32 #include <utils/hostosinfo.h>
33 #include <utils/qtcassert.h>
34 #include <utils/qtcprocess.h>
35 #include <utils/temporaryfile.h>
36 
37 #include <QDebug>
38 #include <QDir>
39 #include <QEventLoop>
40 
41 #define CALLGRIND_CONTROL_DEBUG 0
42 
43 using namespace ProjectExplorer;
44 using namespace Utils;
45 
46 namespace Valgrind {
47 namespace Callgrind {
48 
49 const char CALLGRIND_CONTROL_BINARY[] = "callgrind_control";
50 
51 CallgrindController::CallgrindController() = default;
52 
~CallgrindController()53 CallgrindController::~CallgrindController()
54 {
55     cleanupTempFile();
56 }
57 
toOptionString(CallgrindController::Option option)58 static QString toOptionString(CallgrindController::Option option)
59 {
60     /* callgrind_control help from v3.9.0
61 
62     Options:
63     -h --help        Show this help text
64     --version        Show version
65     -s --stat        Show statistics
66     -b --back        Show stack/back trace
67     -e [<A>,...]     Show event counters for <A>,... (default: all)
68     --dump[=<s>]     Request a dump optionally using <s> as description
69     -z --zero        Zero all event counters
70     -k --kill        Kill
71     --instr=<on|off> Switch instrumentation state on/off
72     */
73 
74     switch (option) {
75         case CallgrindController::Dump:
76             return QLatin1String("--dump");
77         case CallgrindController::ResetEventCounters:
78             return QLatin1String("--zero");
79         case CallgrindController::Pause:
80             return QLatin1String("--instr=off");
81         case CallgrindController::UnPause:
82             return QLatin1String("--instr=on");
83         default:
84             return QString(); // never reached
85     }
86 }
87 
run(Option option)88 void CallgrindController::run(Option option)
89 {
90     if (m_controllerProcess) {
91         emit statusMessage(tr("Previous command has not yet finished."));
92         return;
93     }
94 
95     // save back current running operation
96     m_lastOption = option;
97 
98     m_controllerProcess = new ApplicationLauncher;
99 
100     switch (option) {
101         case CallgrindController::Dump:
102             emit statusMessage(tr("Dumping profile data..."));
103             break;
104         case CallgrindController::ResetEventCounters:
105             emit statusMessage(tr("Resetting event counters..."));
106             break;
107         case CallgrindController::Pause:
108             emit statusMessage(tr("Pausing instrumentation..."));
109             break;
110         case CallgrindController::UnPause:
111             emit statusMessage(tr("Unpausing instrumentation..."));
112             break;
113         default:
114             break;
115     }
116 
117 #if CALLGRIND_CONTROL_DEBUG
118     m_controllerProcess->setProcessChannelMode(QProcess::ForwardedChannels);
119 #endif
120     connect(m_controllerProcess, &ApplicationLauncher::processExited,
121             this, &CallgrindController::controllerProcessFinished);
122     connect(m_controllerProcess, &ApplicationLauncher::error,
123             this, &CallgrindController::handleControllerProcessError);
124     connect(m_controllerProcess, &ApplicationLauncher::finished,
125             this, &CallgrindController::controllerProcessClosed);
126 
127     Runnable controller = m_valgrindRunnable;
128     controller.executable =  FilePath::fromString(CALLGRIND_CONTROL_BINARY);
129     controller.commandLineArguments = QString("%1 %2").arg(toOptionString(option)).arg(m_pid);
130 
131     if (!m_valgrindRunnable.device
132             || m_valgrindRunnable.device->type() == ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE)
133         m_controllerProcess->start(controller);
134     else
135         m_controllerProcess->start(controller, m_valgrindRunnable.device);
136 }
137 
setValgrindPid(qint64 pid)138 void CallgrindController::setValgrindPid(qint64 pid)
139 {
140     m_pid = pid;
141 }
142 
handleControllerProcessError(QProcess::ProcessError)143 void CallgrindController::handleControllerProcessError(QProcess::ProcessError)
144 {
145     QTC_ASSERT(m_controllerProcess, return);
146     const QString error = m_controllerProcess->errorString();
147     emit statusMessage(tr("An error occurred while trying to run %1: %2").arg(CALLGRIND_CONTROL_BINARY).arg(error));
148 
149     m_controllerProcess->deleteLater();
150     m_controllerProcess = nullptr;
151 }
152 
controllerProcessFinished(int rc,QProcess::ExitStatus status)153 void CallgrindController::controllerProcessFinished(int rc, QProcess::ExitStatus status)
154 {
155     QTC_ASSERT(m_controllerProcess, return);
156     const QString error = m_controllerProcess->errorString();
157 
158     m_controllerProcess->deleteLater(); // Called directly from finished() signal in m_process
159     m_controllerProcess = nullptr;
160 
161     if (rc != 0 || status != QProcess::NormalExit) {
162         qWarning() << "Controller exited abnormally:" << error;
163         return;
164     }
165 
166     // this call went fine, we might run another task after this
167     switch (m_lastOption) {
168         case ResetEventCounters:
169             // lets dump the new reset profiling info
170             run(Dump);
171             return;
172         case Pause:
173             break;
174         case Dump:
175             emit statusMessage(tr("Callgrind dumped profiling info"));
176             break;
177         case UnPause:
178             emit statusMessage(tr("Callgrind unpaused."));
179             break;
180         default:
181             break;
182     }
183 
184     emit finished(m_lastOption);
185     m_lastOption = Unknown;
186 }
187 
controllerProcessClosed(bool success)188 void CallgrindController::controllerProcessClosed(bool success)
189 {
190     Q_UNUSED(success)
191     //    QTC_ASSERT(m_remote.m_process, return);
192 
193 //    m_remote.m_errorString = m_remote.m_process->errorString();
194 //    if (status == QSsh::SshRemoteProcess::FailedToStart) {
195 //        m_remote.m_error = QProcess::FailedToStart;
196 //        emit ValgrindProcessX::error(QProcess::FailedToStart);
197 //    } else if (status == QSsh::SshRemoteProcess::NormalExit) {
198 //        emit finished(m_remote.m_process->exitCode(), QProcess::NormalExit);
199 //    } else if (status == QSsh::SshRemoteProcess::CrashExit) {
200 //        m_remote.m_error = QProcess::Crashed;
201 //        emit finished(m_remote.m_process->exitCode(), QProcess::CrashExit);
202 //    }
203      controllerProcessFinished(0, QProcess::NormalExit);
204 }
205 
getLocalDataFile()206 void CallgrindController::getLocalDataFile()
207 {
208     // we look for callgrind.out.PID, but there may be updated ones called ~.PID.NUM
209     const QString baseFileName = QString("callgrind.out.%1").arg(m_pid);
210     const QString workingDir = m_valgrindRunnable.workingDirectory;
211     // first, set the to-be-parsed file to callgrind.out.PID
212     QString fileName = workingDir.isEmpty() ? baseFileName : (workingDir + '/' + baseFileName);
213 
214     if (m_valgrindRunnable.device
215             && m_valgrindRunnable.device->type() != ProjectExplorer::Constants::DESKTOP_DEVICE_TYPE) {
216 //        ///TODO: error handling
217 //        emit statusMessage(tr("Downloading remote profile data..."));
218 //        m_ssh = m_valgrindProc->connection();
219 //        // if there are files like callgrind.out.PID.NUM, set it to the most recent one of those
220 //        QString cmd = QString::fromLatin1("ls -t %1* | head -n 1").arg(fileName);
221 //        m_findRemoteFile = m_ssh->createRemoteProcess(cmd.toUtf8());
222 //        connect(m_findRemoteFile.data(), &QSsh::SshRemoteProcess::readyReadStandardOutput,
223 //                this, &CallgrindController::foundRemoteFile);
224 //        m_findRemoteFile->start();
225     } else {
226         const QDir dir(workingDir, QString::fromLatin1("%1.*").arg(baseFileName), QDir::Time);
227         const QStringList outputFiles = dir.entryList();
228         // if there are files like callgrind.out.PID.NUM, set it to the most recent one of those
229         if (!outputFiles.isEmpty())
230             fileName = workingDir + '/' + outputFiles.first();
231 
232         emit localParseDataAvailable(fileName);
233     }
234 }
235 
foundRemoteFile()236 void CallgrindController::foundRemoteFile()
237 {
238     m_remoteFile = m_findRemoteFile->readAllStandardOutput().trimmed();
239 
240     m_sftp = m_ssh->createSftpSession();
241     connect(m_sftp.get(), &QSsh::SftpSession::commandFinished,
242             this, &CallgrindController::sftpJobFinished);
243     connect(m_sftp.get(), &QSsh::SftpSession::started,
244             this, &CallgrindController::sftpInitialized);
245     m_sftp->start();
246 }
247 
sftpInitialized()248 void CallgrindController::sftpInitialized()
249 {
250     cleanupTempFile();
251     Utils::TemporaryFile dataFile("callgrind.out.");
252     QTC_ASSERT(dataFile.open(), return);
253     m_tempDataFile = dataFile.fileName();
254     dataFile.setAutoRemove(false);
255     dataFile.close();
256 
257     m_downloadJob = m_sftp->downloadFile(QString::fromUtf8(m_remoteFile), m_tempDataFile);
258 }
259 
sftpJobFinished(QSsh::SftpJobId job,const QString & error)260 void CallgrindController::sftpJobFinished(QSsh::SftpJobId job, const QString &error)
261 {
262     QTC_ASSERT(job == m_downloadJob, return);
263 
264     m_sftp->quit();
265 
266     if (error.isEmpty())
267         emit localParseDataAvailable(m_tempDataFile);
268 }
269 
cleanupTempFile()270 void CallgrindController::cleanupTempFile()
271 {
272     if (!m_tempDataFile.isEmpty() && QFile::exists(m_tempDataFile))
273         QFile::remove(m_tempDataFile);
274 
275     m_tempDataFile.clear();
276 }
277 
setValgrindRunnable(const Runnable & runnable)278 void CallgrindController::setValgrindRunnable(const Runnable &runnable)
279 {
280     m_valgrindRunnable = runnable;
281 }
282 
283 } // namespace Callgrind
284 } // namespace Valgrind
285