abstractprocessstep.cpp 14.2 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8 9 10 11 12
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia.  For licensing terms and
Eike Ziller's avatar
Eike Ziller committed
13 14
** conditions see http://www.qt.io/licensing.  For further information
** use the contact form at http://www.qt.io/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
** Alternatively, this file may be used under the terms of the GNU Lesser
Eike Ziller's avatar
Eike Ziller committed
18 19 20 21 22 23
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
hjk's avatar
hjk committed
24 25 26
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
27 28
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
29
****************************************************************************/
hjk's avatar
hjk committed
30

con's avatar
con committed
31
#include "abstractprocessstep.h"
32
#include "ansifilterparser.h"
con's avatar
con committed
33
#include "buildstep.h"
Tobias Hunger's avatar
Tobias Hunger committed
34
#include "ioutputparser.h"
con's avatar
con committed
35
#include "project.h"
36
#include "task.h"
hjk's avatar
hjk committed
37

Tobias Hunger's avatar
Tobias Hunger committed
38
#include <utils/qtcassert.h>
39
#include <utils/qtcprocess.h>
Tobias Hunger's avatar
Tobias Hunger committed
40

41 42
#include <QTimer>
#include <QDir>
con's avatar
con committed
43 44 45

using namespace ProjectExplorer;

46 47 48
/*!
    \class ProjectExplorer::AbstractProcessStep

49 50
    \brief The AbstractProcessStep class is a convenience class that can be
    used as a base class instead of BuildStep.
51 52 53 54 55

    It should be used as a base class if your buildstep just needs to run a process.

    Usage:
    \list
56
    \li Use processParameters() to configure the process you want to run
57
    (you need to do that before calling AbstractProcessStep::init()).
58 59
    \li Inside YourBuildStep::init() call AbstractProcessStep::init().
    \li Inside YourBuildStep::run() call AbstractProcessStep::run(), which automatically starts the process
60
    and by default adds the output on stdOut and stdErr to the OutputWindow.
61
    \li If you need to process the process output override stdOut() and/or stdErr.
62 63 64 65 66 67 68 69 70 71 72 73 74 75
    \endlist

    The two functions processStarted() and processFinished() are called after starting/finishing the process.
    By default they add a message to the output window.

    Use setEnabled() to control whether the BuildStep needs to run. (A disabled BuildStep immediately returns true,
    from the run function.)

    \sa ProjectExplorer::ProcessParameters
*/

/*!
    \fn void ProjectExplorer::AbstractProcessStep::setEnabled(bool b)

76
    Enables or disables a BuildStep.
77

78
    Disabled BuildSteps immediately return true from their run function.
79
    Should be called from init().
80 81 82 83 84
*/

/*!
    \fn ProcessParameters *ProjectExplorer::AbstractProcessStep::processParameters()

85
    Obtains a reference to the parameters for the actual process to run.
86

87
     Should be used in init().
88 89
*/

90
AbstractProcessStep::AbstractProcessStep(BuildStepList *bsl, Core::Id id) :
Tobias Hunger's avatar
Tobias Hunger committed
91
    BuildStep(bsl, id), m_timer(0), m_futureInterface(0),
Daniel Teske's avatar
Daniel Teske committed
92
    m_ignoreReturnValue(false), m_process(0),
93
    m_outputParserChain(0), m_skipFlush(false)
con's avatar
con committed
94
{
95 96
}

Tobias Hunger's avatar
Tobias Hunger committed
97
AbstractProcessStep::AbstractProcessStep(BuildStepList *bsl,
98
                                         AbstractProcessStep *bs) :
Tobias Hunger's avatar
Tobias Hunger committed
99
    BuildStep(bsl, bs), m_timer(0), m_futureInterface(0),
Daniel Teske's avatar
Daniel Teske committed
100
    m_ignoreReturnValue(bs->m_ignoreReturnValue),
101
    m_process(0), m_outputParserChain(0), m_skipFlush(false)
102
{
con's avatar
con committed
103 104
}

Tobias Hunger's avatar
Tobias Hunger committed
105 106 107 108 109 110 111 112
AbstractProcessStep::~AbstractProcessStep()
{
    delete m_process;
    delete m_timer;
    // do not delete m_futureInterface, we do not own it.
    delete m_outputParserChain;
}

113
/*!
114
     Deletes all existing output parsers and starts a new chain with the
115 116 117 118 119
     given parser.

     Derived classes need to call this function.
*/

120
void AbstractProcessStep::setOutputParser(IOutputParser *parser)
Tobias Hunger's avatar
Tobias Hunger committed
121 122
{
    delete m_outputParserChain;
123 124
    m_outputParserChain = new AnsiFilterParser;
    m_outputParserChain->appendOutputParser(parser);
Tobias Hunger's avatar
Tobias Hunger committed
125 126

    if (m_outputParserChain) {
127 128 129 130
        connect(m_outputParserChain, &IOutputParser::addOutput,
                this, &AbstractProcessStep::outputAdded);
        connect(m_outputParserChain, &IOutputParser::addTask,
                this, &AbstractProcessStep::taskAdded);
Tobias Hunger's avatar
Tobias Hunger committed
131 132 133
    }
}

134
/*!
135
    Appends the given output parser to the existing chain of parsers.
136 137
*/

138
void AbstractProcessStep::appendOutputParser(IOutputParser *parser)
Tobias Hunger's avatar
Tobias Hunger committed
139 140 141 142 143 144 145 146 147
{
    if (!parser)
        return;

    QTC_ASSERT(m_outputParserChain, return);
    m_outputParserChain->appendOutputParser(parser);
    return;
}

148
IOutputParser *AbstractProcessStep::outputParser() const
Tobias Hunger's avatar
Tobias Hunger committed
149 150 151 152
{
    return m_outputParserChain;
}

153 154 155 156 157 158
void AbstractProcessStep::emitFaultyConfigurationMessage()
{
    emit addOutput(tr("Configuration is faulty. Check the Issues view for details."),
                   BuildStep::MessageOutput);
}

159 160 161 162 163
bool AbstractProcessStep::ignoreReturnValue()
{
    return m_ignoreReturnValue;
}

164
/*!
165
    If \a ignoreReturnValue is set to true, then the abstractprocess step will
166 167 168 169 170
    return success even if the return value indicates otherwise.

    Should be called from init.
*/

171
void AbstractProcessStep::setIgnoreReturnValue(bool b)
172
{
173
    m_ignoreReturnValue = b;
174 175
}

176
/*!
177 178
    Reimplemented from BuildStep::init(). You need to call this from
    YourBuildStep::init().
179 180
*/

181
bool AbstractProcessStep::init()
con's avatar
con committed
182 183 184 185
{
    return true;
}

186
/*!
187 188
    Reimplemented from BuildStep::init(). You need to call this from
    YourBuildStep::run().
189 190
*/

Tobias Hunger's avatar
Tobias Hunger committed
191
void AbstractProcessStep::run(QFutureInterface<bool> &fi)
con's avatar
con committed
192 193
{
    m_futureInterface = &fi;
194
    QDir wd(m_param.effectiveWorkingDirectory());
195 196 197 198 199 200 201 202 203 204 205
    if (!wd.exists()) {
        if (!wd.mkpath(wd.absolutePath())) {
            emit addOutput(tr("Could not create directory \"%1\"")
                           .arg(QDir::toNativeSeparators(wd.absolutePath())),
                           BuildStep::ErrorMessageOutput);
            fi.reportResult(false);
            emit finished();
            return;
        }
    }

206
    QString effectiveCommand = m_param.effectiveCommand();
207
    if (!QFileInfo::exists(effectiveCommand)) {
208 209 210 211 212 213
        processStartupFailed();
        fi.reportResult(false);
        emit finished();
        return;
    }

214
    m_process = new Utils::QtcProcess();
Eike Ziller's avatar
Eike Ziller committed
215 216
    if (Utils::HostOsInfo::isWindowsHost())
        m_process->setUseCtrlCStub(true);
217
    m_process->setWorkingDirectory(wd.absolutePath());
218
    m_process->setEnvironment(m_param.environment());
con's avatar
con committed
219 220

    connect(m_process, SIGNAL(readyReadStandardOutput()),
221
            this, SLOT(processReadyReadStdOutput()));
con's avatar
con committed
222
    connect(m_process, SIGNAL(readyReadStandardError()),
223
            this, SLOT(processReadyReadStdError()));
con's avatar
con committed
224

Robert Loehning's avatar
Robert Loehning committed
225
    connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)),
226
            this, SLOT(slotProcessFinished(int,QProcess::ExitStatus)));
con's avatar
con committed
227

228
    m_process->setCommand(effectiveCommand, m_param.effectiveArguments());
229
    m_process->start();
hjk's avatar
hjk committed
230
    if (!m_process->waitForStarted()) {
con's avatar
con committed
231 232 233 234
        processStartupFailed();
        delete m_process;
        m_process = 0;
        fi.reportResult(false);
235
        emit finished();
con's avatar
con committed
236 237 238 239 240
        return;
    }
    processStarted();

    m_timer = new QTimer();
241
    connect(m_timer, SIGNAL(timeout()), this, SLOT(checkForCancel()));
con's avatar
con committed
242
    m_timer->start(500);
243 244
    m_killProcess = false;
}
con's avatar
con committed
245

246 247
void AbstractProcessStep::cleanUp()
{
con's avatar
con committed
248
    // The process has finished, leftover data is read in processFinished
249 250
    processFinished(m_process->exitCode(), m_process->exitStatus());
    bool returnValue = processSucceeded(m_process->exitCode(), m_process->exitStatus()) || m_ignoreReturnValue;
con's avatar
con committed
251

252 253 254 255 256 257
    // Clean up output parsers
    if (m_outputParserChain) {
        delete m_outputParserChain;
        m_outputParserChain = 0;
    }

con's avatar
con committed
258 259
    delete m_process;
    m_process = 0;
260
    m_futureInterface->reportResult(returnValue);
261
    m_futureInterface = 0;
262 263

    emit finished();
con's avatar
con committed
264 265
}

266
/*!
267
    Called after the process is started.
268

269 270
    The default implementation adds a process-started message to the output
    message.
271 272
*/

con's avatar
con committed
273 274
void AbstractProcessStep::processStarted()
{
275
    emit addOutput(tr("Starting: \"%1\" %2")
276 277
                   .arg(QDir::toNativeSeparators(m_param.effectiveCommand()),
                        m_param.prettyArguments()),
278
                   BuildStep::MessageOutput);
con's avatar
con committed
279 280
}

281
/*!
282
    Called after the process is finished.
283

284
    The default implementation adds a line to the output window.
285 286
*/

287
void AbstractProcessStep::processFinished(int exitCode, QProcess::ExitStatus status)
con's avatar
con committed
288
{
289 290 291
    if (m_outputParserChain)
        m_outputParserChain->flush();

292
    QString command = QDir::toNativeSeparators(m_param.effectiveCommand());
293
    if (status == QProcess::NormalExit && exitCode == 0) {
Oswald Buddenhagen's avatar
Oswald Buddenhagen committed
294
        emit addOutput(tr("The process \"%1\" exited normally.").arg(command),
295
                       BuildStep::MessageOutput);
296
    } else if (status == QProcess::NormalExit) {
297
        emit addOutput(tr("The process \"%1\" exited with code %2.")
Oswald Buddenhagen's avatar
Oswald Buddenhagen committed
298
                       .arg(command, QString::number(m_process->exitCode())),
299
                       BuildStep::ErrorMessageOutput);
300
    } else {
Oswald Buddenhagen's avatar
Oswald Buddenhagen committed
301
        emit addOutput(tr("The process \"%1\" crashed.").arg(command), BuildStep::ErrorMessageOutput);
302
    }
con's avatar
con committed
303 304
}

305
/*!
306
    Called if the process could not be started.
307

308
    By default, adds a message to the output window.
309 310
*/

con's avatar
con committed
311 312
void AbstractProcessStep::processStartupFailed()
{
313
    emit addOutput(tr("Could not start process \"%1\" %2")
314 315
                   .arg(QDir::toNativeSeparators(m_param.effectiveCommand()),
                        m_param.prettyArguments()),
316
                   BuildStep::ErrorMessageOutput);
317 318
}

319
/*!
320
    Called to test whether a process succeeded or not.
321 322
*/

323 324
bool AbstractProcessStep::processSucceeded(int exitCode, QProcess::ExitStatus status)
{
325 326 327
    if (outputParser() && outputParser()->hasFatalErrors())
        return false;

328
    return exitCode == 0 && status == QProcess::NormalExit;
con's avatar
con committed
329 330 331 332 333
}

void AbstractProcessStep::processReadyReadStdOutput()
{
    m_process->setReadChannel(QProcess::StandardOutput);
hjk's avatar
hjk committed
334
    while (m_process->canReadLine()) {
335
        QString line = QString::fromLocal8Bit(m_process->readLine());
Tobias Hunger's avatar
Tobias Hunger committed
336
        stdOutput(line);
con's avatar
con committed
337 338 339
    }
}

340
/*!
341
    Called for each line of output on stdOut().
342 343 344 345

    The default implementation adds the line to the application output window.
*/

Tobias Hunger's avatar
Tobias Hunger committed
346
void AbstractProcessStep::stdOutput(const QString &line)
con's avatar
con committed
347
{
Tobias Hunger's avatar
Tobias Hunger committed
348 349
    if (m_outputParserChain)
        m_outputParserChain->stdOutput(line);
350
    emit addOutput(line, BuildStep::NormalOutput, BuildStep::DontAppendNewline);
con's avatar
con committed
351 352 353 354 355
}

void AbstractProcessStep::processReadyReadStdError()
{
    m_process->setReadChannel(QProcess::StandardError);
hjk's avatar
hjk committed
356
    while (m_process->canReadLine()) {
357
        QString line = QString::fromLocal8Bit(m_process->readLine());
con's avatar
con committed
358 359 360 361
        stdError(line);
    }
}

362
/*!
363
    Called for each line of output on StdErrror().
364

365
    The default implementation adds the line to the application output window.
366 367
*/

con's avatar
con committed
368 369
void AbstractProcessStep::stdError(const QString &line)
{
Tobias Hunger's avatar
Tobias Hunger committed
370 371
    if (m_outputParserChain)
        m_outputParserChain->stdError(line);
372
    emit addOutput(line, BuildStep::ErrorOutput, BuildStep::DontAppendNewline);
con's avatar
con committed
373 374
}

375 376 377 378 379
QFutureInterface<bool> *AbstractProcessStep::futureInterface() const
{
    return m_futureInterface;
}

con's avatar
con committed
380 381
void AbstractProcessStep::checkForCancel()
{
hjk's avatar
hjk committed
382
    if (m_futureInterface->isCanceled() && m_timer->isActive()) {
383 384 385 386 387 388 389 390
        if (!m_killProcess) {
            m_process->terminate();
            m_timer->start(5000);
            m_killProcess = true;
        } else {
            m_process->kill();
            m_timer->stop();
        }
con's avatar
con committed
391 392 393
    }
}

394
void AbstractProcessStep::taskAdded(const Task &task)
Tobias Hunger's avatar
Tobias Hunger committed
395
{
396 397 398 399 400
    // Do not bother to report issues if we do not care about the results of
    // the buildstep anyway:
    if (m_ignoreReturnValue)
        return;

401 402 403 404 405 406 407
    // flush out any pending tasks before proceeding:
    if (!m_skipFlush && m_outputParserChain) {
        m_skipFlush = true;
        m_outputParserChain->flush();
        m_skipFlush = false;
    }

408
    Task editable(task);
409
    QString filePath = task.file.toString();
Tobias Hunger's avatar
Tobias Hunger committed
410 411 412 413 414 415 416 417 418
    if (!filePath.isEmpty() && !QDir::isAbsolutePath(filePath)) {
        // We have no save way to decide which file in which subfolder
        // is meant. Therefore we apply following heuristics:
        // 1. Check if file is unique in whole project
        // 2. Otherwise try again without any ../
        // 3. give up.

        QList<QFileInfo> possibleFiles;
        QString fileName = QFileInfo(filePath).fileName();
419
        foreach (const QString &file, project()->files(Project::AllFiles)) {
Tobias Hunger's avatar
Tobias Hunger committed
420 421 422 423 424 425
            QFileInfo candidate(file);
            if (candidate.fileName() == fileName)
                possibleFiles << candidate;
        }

        if (possibleFiles.count() == 1) {
426
            editable.file = Utils::FileName(possibleFiles.first());
Tobias Hunger's avatar
Tobias Hunger committed
427 428 429
        } else {
            // More then one filename, so do a better compare
            // Chop of any "../"
430 431
            while (filePath.startsWith(QLatin1String("../")))
                filePath.remove(0, 3);
Tobias Hunger's avatar
Tobias Hunger committed
432 433
            int count = 0;
            QString possibleFilePath;
434
            foreach (const QFileInfo &fi, possibleFiles) {
Tobias Hunger's avatar
Tobias Hunger committed
435 436 437 438 439 440
                if (fi.filePath().endsWith(filePath)) {
                    possibleFilePath = fi.filePath();
                    ++count;
                }
            }
            if (count == 1)
441
                editable.file = Utils::FileName::fromString(possibleFilePath);
Tobias Hunger's avatar
Tobias Hunger committed
442 443 444 445 446 447 448
            else
                qWarning() << "Could not find absolute location of file " << filePath;
        }
    }
    emit addTask(editable);
}

449
void AbstractProcessStep::outputAdded(const QString &string, BuildStep::OutputFormat format)
Tobias Hunger's avatar
Tobias Hunger committed
450
{
451
    emit addOutput(string, format, BuildStep::DontAppendNewline);
Tobias Hunger's avatar
Tobias Hunger committed
452 453
}

con's avatar
con committed
454 455
void AbstractProcessStep::slotProcessFinished(int, QProcess::ExitStatus)
{
456 457 458 459
    m_timer->stop();
    delete m_timer;
    m_timer = 0;

460
    QString line = QString::fromLocal8Bit(m_process->readAllStandardError());
hjk's avatar
hjk committed
461
    if (!line.isEmpty())
dt's avatar
dt committed
462
        stdError(line);
con's avatar
con committed
463

464
    line = QString::fromLocal8Bit(m_process->readAllStandardOutput());
hjk's avatar
hjk committed
465
    if (!line.isEmpty())
Tobias Hunger's avatar
Tobias Hunger committed
466
        stdOutput(line);
hjk's avatar
hjk committed
467

468
    cleanUp();
con's avatar
con committed
469
}