| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2017 The Qt Company Ltd. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #include "qqmldebugprocess_p.h" |
| 30 | |
| 31 | #include <QtCore/qdebug.h> |
| 32 | #include <QtCore/qfileinfo.h> |
| 33 | #include <QtCore/qdir.h> |
| 34 | |
| 35 | QQmlDebugProcess::QQmlDebugProcess(const QString &executable, QObject *parent) |
| 36 | : QObject(parent) |
| 37 | , m_executable(executable) |
| 38 | , m_state(SessionUnknown) |
| 39 | , m_port(0) |
| 40 | , m_maximumBindErrors(0) |
| 41 | , m_receivedBindErrors(0) |
| 42 | { |
| 43 | m_process.setProcessChannelMode(QProcess::MergedChannels); |
| 44 | m_timer.setInterval(15000); |
| 45 | connect(sender: &m_process, signal: &QProcess::readyReadStandardOutput, |
| 46 | receiver: this, slot: &QQmlDebugProcess::processAppOutput); |
| 47 | connect(sender: &m_process, signal: &QProcess::errorOccurred, |
| 48 | receiver: this, slot: &QQmlDebugProcess::processError); |
| 49 | connect(sender: &m_process, signal: QOverload<int, QProcess::ExitStatus>::of(ptr: &QProcess::finished), |
| 50 | context: this, slot: [this]() { |
| 51 | m_timer.stop(); |
| 52 | m_eventLoop.quit(); |
| 53 | emit finished(); |
| 54 | }); |
| 55 | connect(sender: &m_timer, signal: &QTimer::timeout, |
| 56 | receiver: this, slot: &QQmlDebugProcess::timeout); |
| 57 | } |
| 58 | |
| 59 | QQmlDebugProcess::~QQmlDebugProcess() |
| 60 | { |
| 61 | stop(); |
| 62 | } |
| 63 | |
| 64 | QString QQmlDebugProcess::stateString() const |
| 65 | { |
| 66 | QString stateStr; |
| 67 | switch (m_process.state()) { |
| 68 | case QProcess::NotRunning: { |
| 69 | stateStr = "not running" ; |
| 70 | if (m_process.exitStatus() == QProcess::CrashExit) |
| 71 | stateStr += " (crashed!)" ; |
| 72 | else |
| 73 | stateStr += ", return value " + QString::number(m_process.exitCode()); |
| 74 | break; |
| 75 | } |
| 76 | case QProcess::Starting: |
| 77 | stateStr = "starting" ; |
| 78 | break; |
| 79 | case QProcess::Running: |
| 80 | stateStr = "running" ; |
| 81 | break; |
| 82 | } |
| 83 | return stateStr; |
| 84 | } |
| 85 | |
| 86 | void QQmlDebugProcess::start(const QStringList &arguments) |
| 87 | { |
| 88 | #ifdef Q_OS_MAC |
| 89 | // make sure m_executable points to the actual binary even if it's inside an app bundle |
| 90 | QFileInfo binFile(m_executable); |
| 91 | if (!binFile.isExecutable()) { |
| 92 | QDir bundleDir(m_executable + ".app" ); |
| 93 | if (bundleDir.exists()) { |
| 94 | m_executable = bundleDir.absoluteFilePath("Contents/MacOS/" + binFile.baseName()); |
| 95 | //qDebug() << Q_FUNC_INFO << "found bundled binary" << m_executable; |
| 96 | } |
| 97 | } |
| 98 | #endif |
| 99 | m_mutex.lock(); |
| 100 | m_port = 0; |
| 101 | m_process.setEnvironment(QProcess::systemEnvironment() + m_environment); |
| 102 | m_process.start(program: m_executable, arguments); |
| 103 | if (!m_process.waitForStarted()) { |
| 104 | qWarning() << "QML Debug Client: Could not launch app " << m_executable |
| 105 | << ": " << m_process.errorString(); |
| 106 | m_eventLoop.quit(); |
| 107 | } |
| 108 | m_mutex.unlock(); |
| 109 | } |
| 110 | |
| 111 | void QQmlDebugProcess::stop() |
| 112 | { |
| 113 | if (m_process.state() != QProcess::NotRunning) { |
| 114 | disconnect(sender: &m_process, signal: &QProcess::errorOccurred, receiver: this, slot: &QQmlDebugProcess::processError); |
| 115 | m_process.kill(); |
| 116 | m_process.waitForFinished(msecs: 5000); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | void QQmlDebugProcess::setMaximumBindErrors(int ignore) |
| 121 | { |
| 122 | m_maximumBindErrors = ignore; |
| 123 | } |
| 124 | |
| 125 | void QQmlDebugProcess::timeout() |
| 126 | { |
| 127 | qWarning() << "Timeout while waiting for QML debugging messages " |
| 128 | "in application output. Process is in state" << m_process.state() |
| 129 | << ", Output:" << m_output << "." ; |
| 130 | } |
| 131 | |
| 132 | bool QQmlDebugProcess::waitForSessionStart() |
| 133 | { |
| 134 | if (m_process.state() != QProcess::Running) { |
| 135 | qWarning() << "Could not start up " << m_executable; |
| 136 | return false; |
| 137 | } else if (m_state == SessionStarted) { |
| 138 | return true; |
| 139 | } else if (m_state == SessionFailed) { |
| 140 | return false; |
| 141 | } |
| 142 | |
| 143 | m_timer.start(); |
| 144 | m_eventLoop.exec(); |
| 145 | |
| 146 | return m_state == SessionStarted; |
| 147 | } |
| 148 | |
| 149 | int QQmlDebugProcess::debugPort() const |
| 150 | { |
| 151 | return m_port; |
| 152 | } |
| 153 | |
| 154 | bool QQmlDebugProcess::waitForFinished() |
| 155 | { |
| 156 | return m_process.waitForFinished(); |
| 157 | } |
| 158 | |
| 159 | QProcess::ProcessState QQmlDebugProcess::state() const |
| 160 | { |
| 161 | return m_process.state(); |
| 162 | } |
| 163 | |
| 164 | QProcess::ExitStatus QQmlDebugProcess::exitStatus() const |
| 165 | { |
| 166 | return m_process.exitStatus(); |
| 167 | } |
| 168 | |
| 169 | void QQmlDebugProcess::addEnvironment(const QString &environment) |
| 170 | { |
| 171 | m_environment.append(t: environment); |
| 172 | } |
| 173 | |
| 174 | QString QQmlDebugProcess::output() const |
| 175 | { |
| 176 | return m_output; |
| 177 | } |
| 178 | |
| 179 | void QQmlDebugProcess::processAppOutput() |
| 180 | { |
| 181 | m_mutex.lock(); |
| 182 | |
| 183 | bool outputFromAppItself = false; |
| 184 | |
| 185 | QString newOutput = m_process.readAll(); |
| 186 | m_output.append(s: newOutput); |
| 187 | m_outputBuffer.append(s: newOutput); |
| 188 | |
| 189 | while (true) { |
| 190 | const int nlIndex = m_outputBuffer.indexOf(c: QLatin1Char('\n')); |
| 191 | if (nlIndex < 0) // no further complete lines |
| 192 | break; |
| 193 | const QString line = m_outputBuffer.left(n: nlIndex); |
| 194 | m_outputBuffer = m_outputBuffer.right(n: m_outputBuffer.size() - nlIndex - 1); |
| 195 | |
| 196 | if (line.contains(s: "QML Debugger:" )) { |
| 197 | const QRegExp portRx("Waiting for connection on port (\\d+)" ); |
| 198 | if (portRx.indexIn(str: line) != -1) { |
| 199 | m_port = portRx.cap(nth: 1).toInt(); |
| 200 | m_timer.stop(); |
| 201 | m_state = SessionStarted; |
| 202 | m_eventLoop.quit(); |
| 203 | continue; |
| 204 | } |
| 205 | if (line.contains(s: "Unable to listen" )) { |
| 206 | if (++m_receivedBindErrors >= m_maximumBindErrors) { |
| 207 | if (m_maximumBindErrors == 0) |
| 208 | qWarning() << "App was unable to bind to port!" ; |
| 209 | m_timer.stop(); |
| 210 | m_state = SessionFailed; |
| 211 | m_eventLoop.quit(); |
| 212 | } |
| 213 | continue; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | // set to true if there is output not coming from the debugger or we don't understand it |
| 218 | outputFromAppItself = true; |
| 219 | } |
| 220 | m_mutex.unlock(); |
| 221 | |
| 222 | if (outputFromAppItself) |
| 223 | emit readyReadStandardOutput(); |
| 224 | } |
| 225 | |
| 226 | void QQmlDebugProcess::processError(QProcess::ProcessError error) |
| 227 | { |
| 228 | qDebug() << "An error occurred while waiting for debug process to become available:" ; |
| 229 | switch (error) { |
| 230 | case QProcess::FailedToStart: |
| 231 | qDebug() << "Process failed to start." ; |
| 232 | break; |
| 233 | case QProcess::Crashed: |
| 234 | qDebug() << "Process crashed." ; |
| 235 | break; |
| 236 | case QProcess::Timedout: |
| 237 | qDebug() << "Process timed out." ; |
| 238 | break; |
| 239 | case QProcess::WriteError: |
| 240 | qDebug() << "Error while writing to process." ; |
| 241 | break; |
| 242 | case QProcess::ReadError: |
| 243 | qDebug() << "Error while reading from process." ; |
| 244 | break; |
| 245 | case QProcess::UnknownError: |
| 246 | qDebug() << "Unknown process error." ; |
| 247 | break; |
| 248 | } |
| 249 | } |
| 250 | |