/**************************************************************************** ** ** Copyright (C) 2016 Brian McGillion and Hugues Delorme ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** 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 The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "vcscommand.h" #include "vcsbaseplugin.h" #include "vcsoutputwindow.h" #include "vcsplugin.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include /*! \fn void Utils::ProgressParser::parseProgress(const QString &text) Reimplement to parse progress as it appears in the standard output. If a progress string is detected, call \c setProgressAndMaximum() to update the progress bar accordingly. \sa Utils::ProgressParser::setProgressAndMaximum() */ /*! \fn void Utils::ProgressParser::setProgressAndMaximum(int value, int maximum) Sets progress \a value and \a maximum for current command. Called by \c parseProgress() when a progress string is detected. */ using namespace Core; using namespace Utils; namespace VcsBase { namespace Internal { class VcsCommandPrivate { public: struct Job { CommandLine command; int timeoutS = 10; FilePath workingDirectory; ExitCodeInterpreter exitCodeInterpreter = {}; }; VcsCommandPrivate(const FilePath &defaultWorkingDirectory, const Environment &environment) : m_defaultWorkingDirectory(defaultWorkingDirectory), m_environment(environment) { VcsBase::setProcessEnvironment(&m_environment); } ~VcsCommandPrivate() { delete m_progressParser; } Environment environment() { if (!(m_flags & VcsCommand::ForceCLocale)) return m_environment; m_environment.set("LANG", "C"); m_environment.set("LANGUAGE", "C"); return m_environment; } QString m_displayName; const FilePath m_defaultWorkingDirectory; Environment m_environment; QTextCodec *m_codec = nullptr; ProgressParser *m_progressParser = nullptr; QFutureWatcher m_watcher; QList m_jobs; unsigned m_flags = 0; bool m_progressiveOutput = false; bool m_aborted = false; }; } // namespace Internal VcsCommand::VcsCommand(const FilePath &workingDirectory, const Environment &environment) : d(new Internal::VcsCommandPrivate(workingDirectory, environment)) { connect(&d->m_watcher, &QFutureWatcher::canceled, this, &VcsCommand::cancel); VcsOutputWindow::setRepository(d->m_defaultWorkingDirectory); VcsOutputWindow *outputWindow = VcsOutputWindow::instance(); // Keep me here, just to be sure it's not instantiated in other thread connect(this, &VcsCommand::append, outputWindow, [outputWindow](const QString &t) { outputWindow->append(t); }); connect(this, &VcsCommand::appendSilently, outputWindow, &VcsOutputWindow::appendSilently); connect(this, &VcsCommand::appendError, outputWindow, &VcsOutputWindow::appendError); connect(this, &VcsCommand::appendCommand, outputWindow, &VcsOutputWindow::appendCommand); connect(this, &VcsCommand::appendMessage, outputWindow, &VcsOutputWindow::appendMessage); const auto connection = connect(this, &VcsCommand::runCommandFinished, this, &VcsCommand::postRunCommand); connect(ICore::instance(), &ICore::coreAboutToClose, this, [this, connection] { disconnect(connection); abort(); }); } void VcsCommand::addTask(const QFuture &future) { if ((d->m_flags & VcsCommand::SuppressCommandLogging)) return; const QString name = displayName(); const auto id = Id::fromString(name + QLatin1String(".action")); if (d->m_progressParser) { ProgressManager::addTask(future, name, id); } else { ProgressManager::addTimedTask(QFutureInterface::get(future), name, id, qMax(2, timeoutS() / 5)); } Internal::VcsPlugin::addFuture(future); } void VcsCommand::postRunCommand(const FilePath &workingDirectory) { if (!(d->m_flags & VcsCommand::ExpectRepoChanges)) return; // TODO tell the document manager that the directory now received all expected changes // Core::DocumentManager::unexpectDirectoryChange(d->m_workingDirectory); VcsManager::emitRepositoryChanged(workingDirectory); } VcsCommand::~VcsCommand() { d->m_watcher.cancel(); delete d; } QString VcsCommand::displayName() const { if (!d->m_displayName.isEmpty()) return d->m_displayName; if (!d->m_jobs.isEmpty()) { const Internal::VcsCommandPrivate::Job &job = d->m_jobs.at(0); QString result = job.command.executable().baseName(); if (!result.isEmpty()) result[0] = result.at(0).toTitleCase(); else result = tr("UNKNOWN"); if (!job.command.arguments().isEmpty()) result += ' ' + job.command.splitArguments().at(0); return result; } return tr("Unknown"); } void VcsCommand::setDisplayName(const QString &name) { d->m_displayName = name; } void VcsCommand::addFlags(unsigned f) { d->m_flags |= f; } void VcsCommand::addJob(const CommandLine &command, int timeoutS, const FilePath &workingDirectory, const ExitCodeInterpreter &interpreter) { d->m_jobs.push_back({command, timeoutS, !workingDirectory.isEmpty() ? workingDirectory : d->m_defaultWorkingDirectory, interpreter}); } void VcsCommand::execute() { if (d->m_jobs.empty()) return; QFuture task = runAsync(&VcsCommand::run, this); d->m_watcher.setFuture(task); addTask(task); } void VcsCommand::abort() { d->m_aborted = true; d->m_watcher.future().cancel(); } void VcsCommand::cancel() { emit terminate(); } int VcsCommand::timeoutS() const { return std::accumulate(d->m_jobs.cbegin(), d->m_jobs.cend(), 0, [](int sum, const Internal::VcsCommandPrivate::Job &job) { return sum + job.timeoutS; }); } void VcsCommand::run(QFutureInterface &future) { // Check that the binary path is not empty QTC_ASSERT(!d->m_jobs.isEmpty(), return); QString stdOut; QString stdErr; if (d->m_flags & VcsCommand::ExpectRepoChanges) { QMetaObject::invokeMethod(this, [] { GlobalFileChangeBlocker::instance()->forceBlocked(true); }); } if (d->m_progressParser) d->m_progressParser->setFuture(&future); else future.setProgressRange(0, 1); const int count = d->m_jobs.size(); bool lastExecSuccess = true; for (int j = 0; j < count; j++) { const Internal::VcsCommandPrivate::Job &job = d->m_jobs.at(j); const CommandResult result = runCommand(job.command, job.timeoutS, job.workingDirectory, job.exitCodeInterpreter); stdOut += result.cleanedStdOut(); stdErr += result.cleanedStdErr(); lastExecSuccess = result.result() == ProcessResult::FinishedWithSuccess; if (!lastExecSuccess) break; } if (!d->m_aborted) { if (!d->m_progressiveOutput) { emit stdOutText(stdOut); if (!stdErr.isEmpty()) emit stdErrText(stdErr); } if (d->m_flags & VcsCommand::ExpectRepoChanges) { QMetaObject::invokeMethod(this, [] { GlobalFileChangeBlocker::instance()->forceBlocked(false); }); } emit finished(lastExecSuccess); if (lastExecSuccess) future.setProgressValue(future.progressMaximum()); else future.cancel(); // sets the progress indicator red } if (d->m_progressParser) d->m_progressParser->setFuture(nullptr); // As it is used asynchronously, we need to delete ourselves this->deleteLater(); } CommandResult VcsCommand::runCommand(const Utils::CommandLine &command, int timeoutS) { return runCommand(command, timeoutS, d->m_defaultWorkingDirectory, {}); } CommandResult VcsCommand::runCommand(const CommandLine &command, int timeoutS, const FilePath &workingDirectory, const ExitCodeInterpreter &interpreter) { QtcProcess proc; if (command.executable().isEmpty()) return {}; proc.setExitCodeInterpreter(interpreter); proc.setTimeoutS(timeoutS); if (!workingDirectory.isEmpty()) proc.setWorkingDirectory(workingDirectory); if (!(d->m_flags & SuppressCommandLogging)) emit appendCommand(workingDirectory, command); proc.setCommand(command); proc.setDisableUnixTerminal(); proc.setEnvironment(d->environment()); if (d->m_flags & MergeOutputChannels) proc.setProcessChannelMode(QProcess::MergedChannels); if (d->m_codec) proc.setCodec(d->m_codec); if ((d->m_flags & FullySynchronously) || (!(d->m_flags & NoFullySync) && QThread::currentThread() == QCoreApplication::instance()->thread())) { runFullySynchronous(proc); } else { runSynchronous(proc); } if (!d->m_aborted) { // Success/Fail message in appropriate window? if (proc.result() == ProcessResult::FinishedWithSuccess) { if (d->m_flags & ShowSuccessMessage) emit appendMessage(proc.exitMessage()); } else if (!(d->m_flags & SuppressFailMessage)) { emit appendError(proc.exitMessage()); } } emit runCommandFinished(workingDirectory); return CommandResult(proc); } void VcsCommand::runFullySynchronous(QtcProcess &process) { process.runBlocking(); if (!d->m_aborted) { const QString stdErr = process.cleanedStdErr(); if (!stdErr.isEmpty() && !(d->m_flags & SuppressStdErr)) emit append(stdErr); const QString stdOut = process.cleanedStdOut(); if (!stdOut.isEmpty() && d->m_flags & ShowStdOut) { if (d->m_flags & SilentOutput) emit appendSilently(stdOut); else emit append(stdOut); } } } void VcsCommand::runSynchronous(QtcProcess &process) { connect(this, &VcsCommand::terminate, &process, [&process] { process.stop(); process.waitForFinished(); }); // connect stderr to the output window if desired if (!(d->m_flags & MergeOutputChannels) && (d->m_progressiveOutput || !(d->m_flags & SuppressStdErr))) { process.setStdErrCallback([this](const QString &text) { if (d->m_progressParser) d->m_progressParser->parseProgress(text); if (!(d->m_flags & SuppressStdErr)) emit appendError(text); if (d->m_progressiveOutput) emit stdErrText(text); }); } // connect stdout to the output window if desired if (d->m_progressParser || d->m_progressiveOutput || (d->m_flags & ShowStdOut)) { process.setStdOutCallback([this](const QString &text) { if (d->m_progressParser) d->m_progressParser->parseProgress(text); if (d->m_flags & ShowStdOut) emit append(text); if (d->m_progressiveOutput) emit stdOutText(text); }); } process.setTimeOutMessageBoxEnabled(true); process.runBlocking(EventLoopMode::On); } void VcsCommand::setCodec(QTextCodec *codec) { d->m_codec = codec; } //! Use \a parser to parse progress data from stdout. Command takes ownership of \a parser void VcsCommand::setProgressParser(ProgressParser *parser) { QTC_ASSERT(!d->m_progressParser, return); d->m_progressParser = parser; } void VcsCommand::setProgressiveOutput(bool progressive) { d->m_progressiveOutput = progressive; } ProgressParser::ProgressParser() : m_futureMutex(new QMutex) { } ProgressParser::~ProgressParser() { delete m_futureMutex; } void ProgressParser::setProgressAndMaximum(int value, int maximum) { QMutexLocker lock(m_futureMutex); if (!m_future) return; m_future->setProgressRange(0, maximum); m_future->setProgressValue(value); } void ProgressParser::setFuture(QFutureInterface *future) { QMutexLocker lock(m_futureMutex); m_future = future; } CommandResult::CommandResult(const QtcProcess &process) : m_result(process.result()) , m_exitCode(process.exitCode()) , m_exitMessage(process.exitMessage()) , m_cleanedStdOut(process.cleanedStdOut()) , m_cleanedStdErr(process.cleanedStdErr()) , m_rawStdOut(process.rawStdOut()) {} } // namespace VcsBase