/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** 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 "qtcprocess.h" #include "stringutils.h" #include "executeondestruction.h" #include "hostosinfo.h" #include "commandline.h" #include "qtcassert.h" #include #include #include #include #include #include #include #ifdef QT_GUI_LIB // qmlpuppet does not use that. #include #include #endif #include #include #include #ifdef Q_OS_WIN #define CALLBACK WINAPI #include #else #include #include #include #endif using namespace Utils::Internal; namespace Utils { namespace Internal { enum { debug = 0 }; enum { syncDebug = 0 }; enum { defaultMaxHangTimerCount = 10 }; static Q_LOGGING_CATEGORY(processLog, "qtc.utils.synchronousprocess", QtWarningMsg); static std::function s_remoteRunProcessHook; // Data for one channel buffer (stderr/stdout) class ChannelBuffer : public QObject { public: void clearForRun(); QString linesRead(); void append(const QByteArray &text, bool emitSignals); QByteArray rawData; QString incompleteLineBuffer; // lines not yet signaled QTextCodec *codec = nullptr; // Not owner std::unique_ptr codecState; int rawDataPos = 0; std::function outputCallback; }; class QtcProcessPrivate : public QObject { public: explicit QtcProcessPrivate(QtcProcess *parent) : q(parent) {} void setupChildProcess_impl(); CommandLine m_commandLine; Environment m_environment; bool m_haveEnv = false; bool m_useCtrlCStub = false; bool m_lowPriority = false; bool m_disableUnixTerminal = false; QProcess::OpenMode m_openMode = QProcess::ReadWrite; // SynchronousProcess left overs: void slotTimeout(); void slotFinished(int exitCode, QProcess::ExitStatus e); void slotError(QProcess::ProcessError); void clearForRun(); SynchronousProcessResponse::Result interpretExitCode(int exitCode); QtcProcess *q; QTextCodec *m_codec = QTextCodec::codecForLocale(); QTimer m_timer; QEventLoop m_eventLoop; SynchronousProcessResponse m_result; FilePath m_binary; ChannelBuffer m_stdOut; ChannelBuffer m_stdErr; ExitCodeInterpreter m_exitCodeInterpreter; int m_hangTimerCount = 0; int m_maxHangTimerCount = defaultMaxHangTimerCount; bool m_startFailure = false; bool m_timeOutMessageBoxEnabled = false; bool m_waitingForUser = false; }; void QtcProcessPrivate::clearForRun() { m_hangTimerCount = 0; m_stdOut.clearForRun(); m_stdOut.codec = m_codec; m_stdErr.clearForRun(); m_stdErr.codec = m_codec; m_result.clear(); m_result.codec = m_codec; m_startFailure = false; m_binary = {}; } SynchronousProcessResponse::Result QtcProcessPrivate::interpretExitCode(int exitCode) { if (m_exitCodeInterpreter) return m_exitCodeInterpreter(exitCode); // default: return exitCode ? SynchronousProcessResponse::FinishedError : SynchronousProcessResponse::Finished; } } // Internal /*! \class Utils::QtcProcess \brief The QtcProcess class provides functionality for with processes. \sa Utils::ProcessArgs */ QtcProcess::QtcProcess(QObject *parent) : QProcess(parent), d(new QtcProcessPrivate(this)) { static int qProcessExitStatusMeta = qRegisterMetaType(); static int qProcessProcessErrorMeta = qRegisterMetaType(); Q_UNUSED(qProcessExitStatusMeta) Q_UNUSED(qProcessProcessErrorMeta) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) && defined(Q_OS_UNIX) setChildProcessModifier([this] { d->setupChildProcess_impl(); }); #endif } QtcProcess::~QtcProcess() { delete d; } void QtcProcess::setEnvironment(const Environment &env) { d->m_environment = env; d->m_haveEnv = true; } const Environment &QtcProcess::environment() const { return d->m_environment; } void QtcProcess::setCommand(const CommandLine &cmdLine) { d->m_commandLine = cmdLine; } const CommandLine &QtcProcess::commandLine() const { return d->m_commandLine; } void QtcProcess::setUseCtrlCStub(bool enabled) { // Do not use the stub in debug mode. Activating the stub will shut down // Qt Creator otherwise, because they share the same Windows console. // See QTCREATORBUG-11995 for details. #ifndef QT_DEBUG d->m_useCtrlCStub = enabled; #else Q_UNUSED(enabled) #endif } void QtcProcess::start() { if (d->m_commandLine.executable().needsDevice()) { QTC_ASSERT(s_remoteRunProcessHook, return); s_remoteRunProcessHook(*this); return; } Environment env; const OsType osType = HostOsInfo::hostOs(); if (d->m_haveEnv) { if (d->m_environment.size() == 0) qWarning("QtcProcess::start: Empty environment set when running '%s'.", qPrintable(d->m_commandLine.executable().toString())); env = d->m_environment; QProcess::setProcessEnvironment(env.toProcessEnvironment()); } else { env = Environment::systemEnvironment(); } const QString &workDir = workingDirectory(); QString command; ProcessArgs arguments; bool success = ProcessArgs::prepareCommand(d->m_commandLine.executable().toString(), d->m_commandLine.arguments(), &command, &arguments, osType, &env, &workDir); if (osType == OsTypeWindows) { QString args; if (d->m_useCtrlCStub) { if (d->m_lowPriority) ProcessArgs::addArg(&args, "-nice"); ProcessArgs::addArg(&args, QDir::toNativeSeparators(command)); command = QCoreApplication::applicationDirPath() + QLatin1String("/qtcreator_ctrlc_stub.exe"); } else if (d->m_lowPriority) { #ifdef Q_OS_WIN setCreateProcessArgumentsModifier([](CreateProcessArguments *args) { args->flags |= BELOW_NORMAL_PRIORITY_CLASS; }); #endif } ProcessArgs::addArgs(&args, arguments.toWindowsArgs()); #ifdef Q_OS_WIN setNativeArguments(args); #endif // Note: Arguments set with setNativeArgs will be appended to the ones // passed with start() below. QProcess::start(command, QStringList(), d->m_openMode); } else { if (!success) { setErrorString(tr("Error in command line.")); // Should be FailedToStart, but we cannot set the process error from the outside, // so it would be inconsistent. emit errorOccurred(QProcess::UnknownError); return; } QProcess::start(command, arguments.toUnixArgs(), d->m_openMode); } } #ifdef Q_OS_WIN static BOOL sendMessage(UINT message, HWND hwnd, LPARAM lParam) { DWORD dwProcessID; GetWindowThreadProcessId(hwnd, &dwProcessID); if ((DWORD)lParam == dwProcessID) { SendNotifyMessage(hwnd, message, 0, 0); return FALSE; } return TRUE; } BOOL CALLBACK sendShutDownMessageToAllWindowsOfProcess_enumWnd(HWND hwnd, LPARAM lParam) { static UINT uiShutDownMessage = RegisterWindowMessage(L"qtcctrlcstub_shutdown"); return sendMessage(uiShutDownMessage, hwnd, lParam); } BOOL CALLBACK sendInterruptMessageToAllWindowsOfProcess_enumWnd(HWND hwnd, LPARAM lParam) { static UINT uiInterruptMessage = RegisterWindowMessage(L"qtcctrlcstub_interrupt"); return sendMessage(uiInterruptMessage, hwnd, lParam); } #endif void QtcProcess::terminate() { #ifdef Q_OS_WIN if (d->m_useCtrlCStub) EnumWindows(sendShutDownMessageToAllWindowsOfProcess_enumWnd, processId()); else #endif QProcess::terminate(); } void QtcProcess::interrupt() { #ifdef Q_OS_WIN QTC_ASSERT(d->m_useCtrlCStub, return); EnumWindows(sendInterruptMessageToAllWindowsOfProcess_enumWnd, processId()); #endif } void QtcProcess::setLowPriority() { d->m_lowPriority = true; } void QtcProcess::setDisableUnixTerminal() { d->m_disableUnixTerminal = true; } void QtcProcess::setRemoteStartProcessHook(const std::function &hook) { s_remoteRunProcessHook = hook; } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) void QtcProcess::setupChildProcess() { d->setupChildProcess_impl(); } #endif void QtcProcessPrivate::setupChildProcess_impl() { #if defined Q_OS_UNIX // nice value range is -20 to +19 where -20 is highest, 0 default and +19 is lowest if (m_lowPriority) { errno = 0; if (::nice(5) == -1 && errno != 0) perror("Failed to set nice value"); } // Disable terminal by becoming a session leader. if (m_disableUnixTerminal) setsid(); #endif } void QtcProcess::setOpenMode(OpenMode mode) { d->m_openMode = mode; } bool QtcProcess::stopProcess() { if (state() == QProcess::NotRunning) return true; terminate(); if (waitForFinished(300)) return true; kill(); return waitForFinished(300); } static bool askToKill(const QString &command) { #ifdef QT_GUI_LIB if (QThread::currentThread() != QCoreApplication::instance()->thread()) return true; const QString title = QtcProcess::tr("Process not Responding"); QString msg = command.isEmpty() ? QtcProcess::tr("The process is not responding.") : QtcProcess::tr("The process \"%1\" is not responding.").arg(command); msg += ' '; msg += QtcProcess::tr("Would you like to terminate it?"); // Restore the cursor that is set to wait while running. const bool hasOverrideCursor = QApplication::overrideCursor() != nullptr; if (hasOverrideCursor) QApplication::restoreOverrideCursor(); QMessageBox::StandardButton answer = QMessageBox::question(nullptr, title, msg, QMessageBox::Yes|QMessageBox::No); if (hasOverrideCursor) QApplication::setOverrideCursor(Qt::WaitCursor); return answer == QMessageBox::Yes; #else Q_UNUSED(command) return true; #endif } // Helper for running a process synchronously in the foreground with timeout // detection similar SynchronousProcess' handling (taking effect after no more output // occurs on stderr/stdout as opposed to waitForFinished()). Returns false if a timeout // occurs. Checking of the process' exit state/code still has to be done. bool QtcProcess::readDataFromProcess(int timeoutS, QByteArray *stdOut, QByteArray *stdErr, bool showTimeOutMessageBox) { enum { syncDebug = 0 }; if (syncDebug) qDebug() << ">readDataFromProcess" << timeoutS; if (state() != QProcess::Running) { qWarning("readDataFromProcess: Process in non-running state passed in."); return false; } QTC_ASSERT(readChannel() == QProcess::StandardOutput, return false); // Keep the process running until it has no longer has data bool finished = false; bool hasData = false; do { finished = waitForFinished(timeoutS > 0 ? timeoutS * 1000 : -1) || state() == QProcess::NotRunning; // First check 'stdout' if (bytesAvailable()) { // applies to readChannel() only hasData = true; const QByteArray newStdOut = readAllStandardOutput(); if (stdOut) stdOut->append(newStdOut); } // Check 'stderr' separately. This is a special handling // for 'git pull' and the like which prints its progress on stderr. const QByteArray newStdErr = readAllStandardError(); if (!newStdErr.isEmpty()) { hasData = true; if (stdErr) stdErr->append(newStdErr); } // Prompt user, pretend we have data if says 'No'. const bool hang = !hasData && !finished; hasData = hang && showTimeOutMessageBox && !askToKill(program()); } while (hasData && !finished); if (syncDebug) qDebug() << "toUnicode(rawStdOut)); } QString SynchronousProcessResponse::stdErr() const { return QtcProcess::normalizeNewlines(codec->toUnicode(rawStdErr)); } QTCREATOR_UTILS_EXPORT QDebug operator<<(QDebug str, const SynchronousProcessResponse& r) { QDebug nsp = str.nospace(); nsp << "SynchronousProcessResponse: result=" << r.result << " ex=" << r.exitCode << '\n' << r.rawStdOut.size() << " bytes stdout, stderr=" << r.rawStdErr << '\n'; return str; } void ChannelBuffer::clearForRun() { rawDataPos = 0; rawData.clear(); codecState.reset(new QTextCodec::ConverterState); incompleteLineBuffer.clear(); } /* Check for complete lines read from the device and return them, moving the * buffer position. */ QString ChannelBuffer::linesRead() { // Convert and append the new input to the buffer of incomplete lines const char *start = rawData.constData() + rawDataPos; const int len = rawData.size() - rawDataPos; incompleteLineBuffer.append(codec->toUnicode(start, len, codecState.get())); rawDataPos = rawData.size(); // Any completed lines in the incompleteLineBuffer? const int lastLineIndex = qMax(incompleteLineBuffer.lastIndexOf('\n'), incompleteLineBuffer.lastIndexOf('\r')); if (lastLineIndex == -1) return QString(); // Get completed lines and remove them from the incompleteLinesBuffer: const QString lines = QtcProcess::normalizeNewlines(incompleteLineBuffer.left(lastLineIndex + 1)); incompleteLineBuffer = incompleteLineBuffer.mid(lastLineIndex + 1); return lines; } void ChannelBuffer::append(const QByteArray &text, bool emitSignals) { if (text.isEmpty()) return; rawData += text; if (!emitSignals) return; // Buffered. Emit complete lines? if (outputCallback) { const QString lines = linesRead(); if (!lines.isEmpty()) outputCallback(lines); } } // ----------- SynchronousProcess SynchronousProcess::SynchronousProcess() { d->m_timer.setInterval(1000); connect(&d->m_timer, &QTimer::timeout, d, &QtcProcessPrivate::slotTimeout); connect(this, QOverload::of(&QProcess::finished), d, &QtcProcessPrivate::slotFinished); connect(this, &QProcess::errorOccurred, d, &QtcProcessPrivate::slotError); connect(this, &QProcess::readyReadStandardOutput, d, [this] { d->m_hangTimerCount = 0; d->m_stdOut.append(readAllStandardOutput(), true); }); connect(this, &QProcess::readyReadStandardError, d, [this] { d->m_hangTimerCount = 0; d->m_stdErr.append(readAllStandardError(), true); }); } SynchronousProcess::~SynchronousProcess() { disconnect(&d->m_timer, nullptr, this, nullptr); disconnect(this, nullptr, this, nullptr); } void QtcProcess::setTimeoutS(int timeoutS) { if (timeoutS > 0) d->m_maxHangTimerCount = qMax(2, timeoutS); else d->m_maxHangTimerCount = INT_MAX / 1000; } void QtcProcess::setCodec(QTextCodec *c) { QTC_ASSERT(c, return); d->m_codec = c; } void QtcProcess::setTimeOutMessageBoxEnabled(bool v) { d->m_timeOutMessageBoxEnabled = v; } void QtcProcess::setExitCodeInterpreter(const ExitCodeInterpreter &interpreter) { d->m_exitCodeInterpreter = interpreter; } #ifdef QT_GUI_LIB static bool isGuiThread() { return QThread::currentThread() == QCoreApplication::instance()->thread(); } #endif SynchronousProcessResponse QtcProcess::run(const CommandLine &cmd, const QByteArray &writeData) { // FIXME: Implement properly if (cmd.executable().needsDevice()) { QtcProcess proc; proc.setEnvironment(Environment(environment())); proc.setWorkingDirectory(workingDirectory()); proc.setCommand(cmd); // writeData ? proc.start(); proc.waitForFinished(); SynchronousProcessResponse res; res.result = SynchronousProcessResponse::Finished; res.exitCode = proc.exitCode(); res.rawStdOut = proc.readAllStandardOutput(); res.rawStdErr = proc.readAllStandardError(); return res; }; qCDebug(processLog).noquote() << "Starting:" << cmd.toUserOutput(); ExecuteOnDestruction logResult([this] { qCDebug(processLog) << d->m_result; }); d->clearForRun(); d->m_binary = cmd.executable(); // using QProcess::start() and passing program, args and OpenMode results in a different // quoting of arguments than using QProcess::setArguments() beforehand and calling start() // only with the OpenMode setCommand(cmd); if (!writeData.isEmpty()) { connect(this, &QProcess::started, this, [this, writeData] { write(writeData); closeWriteChannel(); }); } setOpenMode(writeData.isEmpty() ? QIODevice::ReadOnly : QIODevice::ReadWrite); start(); // On Windows, start failure is triggered immediately if the // executable cannot be found in the path. Do not start the // event loop in that case. if (!d->m_startFailure) { d->m_timer.start(); #ifdef QT_GUI_LIB if (isGuiThread()) QApplication::setOverrideCursor(Qt::WaitCursor); #endif d->m_eventLoop.exec(QEventLoop::ExcludeUserInputEvents); d->m_stdOut.append(readAllStandardOutput(), false); d->m_stdErr.append(readAllStandardError(), false); d->m_result.rawStdOut = d->m_stdOut.rawData; d->m_result.rawStdErr = d->m_stdErr.rawData; d->m_timer.stop(); #ifdef QT_GUI_LIB if (isGuiThread()) QApplication::restoreOverrideCursor(); #endif } return d->m_result; } SynchronousProcessResponse QtcProcess::runBlocking(const CommandLine &cmd) { // FIXME: Implement properly if (cmd.executable().needsDevice()) { QtcProcess proc; proc.setEnvironment(Environment(environment())); proc.setWorkingDirectory(workingDirectory()); proc.setCommand(cmd); // writeData ? proc.start(); proc.waitForFinished(); SynchronousProcessResponse res; res.result = SynchronousProcessResponse::Finished; res.exitCode = proc.exitCode(); res.rawStdOut = proc.readAllStandardOutput(); res.rawStdErr = proc.readAllStandardError(); return res; }; qCDebug(processLog).noquote() << "Starting blocking:" << cmd.toUserOutput(); ExecuteOnDestruction logResult([this] { qCDebug(processLog) << d->m_result; }); d->clearForRun(); d->m_binary = cmd.executable(); setOpenMode(QIODevice::ReadOnly); setCommand(cmd); start(); if (!waitForStarted(d->m_maxHangTimerCount * 1000)) { d->m_result.result = SynchronousProcessResponse::StartFailed; return d->m_result; } closeWriteChannel(); if (!waitForFinished(d->m_maxHangTimerCount * 1000)) { d->m_result.result = SynchronousProcessResponse::Hang; terminate(); if (!waitForFinished(1000)) { kill(); waitForFinished(1000); } } if (state() != QProcess::NotRunning) return d->m_result; d->m_result.exitCode = exitCode(); if (d->m_result.result == SynchronousProcessResponse::StartFailed) { if (exitStatus() != QProcess::NormalExit) d->m_result.result = SynchronousProcessResponse::TerminatedAbnormally; else d->m_result.result = d->interpretExitCode(d->m_result.exitCode); } d->m_stdOut.append(readAllStandardOutput(), false); d->m_stdErr.append(readAllStandardError(), false); d->m_result.rawStdOut = d->m_stdOut.rawData; d->m_result.rawStdErr = d->m_stdErr.rawData; return d->m_result; } void QtcProcess::setStdOutCallback(const std::function &callback) { d->m_stdOut.outputCallback = callback; } void QtcProcess::setStdErrCallback(const std::function &callback) { d->m_stdErr.outputCallback = callback; } void QtcProcessPrivate::slotTimeout() { if (!m_waitingForUser && (++m_hangTimerCount > m_maxHangTimerCount)) { if (debug) qDebug() << Q_FUNC_INFO << "HANG detected, killing"; m_waitingForUser = true; const bool terminate = !m_timeOutMessageBoxEnabled || askToKill(m_binary.toString()); m_waitingForUser = false; if (terminate) { q->stopProcess(); m_result.result = SynchronousProcessResponse::Hang; } else { m_hangTimerCount = 0; } } else { if (debug) qDebug() << Q_FUNC_INFO << m_hangTimerCount; } } void QtcProcessPrivate::slotFinished(int exitCode, QProcess::ExitStatus e) { if (debug) qDebug() << Q_FUNC_INFO << exitCode << e; m_hangTimerCount = 0; switch (e) { case QProcess::NormalExit: m_result.result = interpretExitCode(exitCode); m_result.exitCode = exitCode; break; case QProcess::CrashExit: // Was hang detected before and killed? if (m_result.result != SynchronousProcessResponse::Hang) m_result.result = SynchronousProcessResponse::TerminatedAbnormally; m_result.exitCode = -1; break; } m_eventLoop.quit(); } void QtcProcessPrivate::slotError(QProcess::ProcessError e) { m_hangTimerCount = 0; if (debug) qDebug() << Q_FUNC_INFO << e; // Was hang detected before and killed? if (m_result.result != SynchronousProcessResponse::Hang) m_result.result = SynchronousProcessResponse::StartFailed; m_startFailure = true; m_eventLoop.quit(); } } // namespace Utils