diff options
Diffstat (limited to 'src/plugins/android')
-rw-r--r-- | src/plugins/android/android.pro | 2 | ||||
-rw-r--r-- | src/plugins/android/android.qbs | 2 | ||||
-rw-r--r-- | src/plugins/android/androiddeployqtstep.cpp | 12 | ||||
-rw-r--r-- | src/plugins/android/androidmanager.cpp | 10 | ||||
-rw-r--r-- | src/plugins/android/androidmanager.h | 3 | ||||
-rw-r--r-- | src/plugins/android/androidrunnable.h | 1 | ||||
-rw-r--r-- | src/plugins/android/androidrunner.cpp | 611 | ||||
-rw-r--r-- | src/plugins/android/androidrunner.h | 4 | ||||
-rw-r--r-- | src/plugins/android/androidrunnerworker.cpp | 569 | ||||
-rw-r--r-- | src/plugins/android/androidrunnerworker.h | 134 |
10 files changed, 749 insertions, 599 deletions
diff --git a/src/plugins/android/android.pro b/src/plugins/android/android.pro index 9d2803e551..434966b075 100644 --- a/src/plugins/android/android.pro +++ b/src/plugins/android/android.pro @@ -18,6 +18,7 @@ HEADERS += \ androiderrormessage.h \ androidglobal.h \ androidrunner.h \ + androidrunnerworker.h \ androiddebugsupport.h \ androidqtversionfactory.h \ androidqtversion.h \ @@ -66,6 +67,7 @@ SOURCES += \ androidtoolchain.cpp \ androiderrormessage.cpp \ androidrunner.cpp \ + androidrunnerworker.cpp \ androiddebugsupport.cpp \ androidqtversionfactory.cpp \ androidqtversion.cpp \ diff --git a/src/plugins/android/android.qbs b/src/plugins/android/android.qbs index 8f095013b5..18187f00e0 100644 --- a/src/plugins/android/android.qbs +++ b/src/plugins/android/android.qbs @@ -93,6 +93,8 @@ Project { "androidrunnable.h", "androidrunner.cpp", "androidrunner.h", + "androidrunnerworker.cpp", + "androidrunnerworker.h", "androidsdkmanager.cpp", "androidsdkmanager.h", "androidsdkmanagerwidget.cpp", diff --git a/src/plugins/android/androiddeployqtstep.cpp b/src/plugins/android/androiddeployqtstep.cpp index 5e264117e9..5cf723fe69 100644 --- a/src/plugins/android/androiddeployqtstep.cpp +++ b/src/plugins/android/androiddeployqtstep.cpp @@ -164,6 +164,7 @@ bool AndroidDeployQtStep::init(QList<const BuildStep *> &earlierSteps) m_filesToPull["/system/" + libDirName + "/libc.so"] = buildDir + "libc.so"; AndroidManager::setDeviceSerialNumber(target(), m_serialNumber); + AndroidManager::setDeviceApiLevel(target(), info.sdk); QtSupport::BaseQtVersion *version = QtSupport::QtKitInformation::qtVersion(target()->kit()); @@ -325,11 +326,12 @@ AndroidDeployQtStep::DeployErrorCode AndroidDeployQtStep::runDeploy(QFutureInter emit addOutput(tr("The process \"%1\" crashed.").arg(m_command), BuildStep::OutputFormat::ErrorMessage); } - if (exitCode == 0 && exitStatus == QProcess::NormalExit) { - if (deployError != NoError && m_uninstallPreviousPackageRun) { - deployError = Failure; - } - } else { + if (deployError != NoError) { + if (m_uninstallPreviousPackageRun) + deployError = Failure; // Even re-install failed. Set to Failure. + } else if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + // Set the deployError to Failure when no deployError code was detected + // but the adb tool failed otherwise relay the detected deployError. deployError = Failure; } diff --git a/src/plugins/android/androidmanager.cpp b/src/plugins/android/androidmanager.cpp index c361ce2490..7a757d7f07 100644 --- a/src/plugins/android/androidmanager.cpp +++ b/src/plugins/android/androidmanager.cpp @@ -241,6 +241,16 @@ void AndroidManager::setDeviceSerialNumber(ProjectExplorer::Target *target, cons target->setNamedSettings(AndroidDeviceSn, deviceSerialNumber); } +int AndroidManager::deviceApiLevel(ProjectExplorer::Target *target) +{ + return target->namedSettings(ApiLevelKey).toInt(); +} + +void AndroidManager::setDeviceApiLevel(ProjectExplorer::Target *target, int level) +{ + target->setNamedSettings(ApiLevelKey, level); +} + QPair<int, int> AndroidManager::apiLevelRange() { return qMakePair(9, 26); diff --git a/src/plugins/android/androidmanager.h b/src/plugins/android/androidmanager.h index d6e175c66f..b1ce332131 100644 --- a/src/plugins/android/androidmanager.h +++ b/src/plugins/android/androidmanager.h @@ -56,6 +56,9 @@ public: static QString deviceSerialNumber(ProjectExplorer::Target *target); static void setDeviceSerialNumber(ProjectExplorer::Target *target, const QString &deviceSerialNumber); + static int deviceApiLevel(ProjectExplorer::Target *target); + static void setDeviceApiLevel(ProjectExplorer::Target *target, int level); + static QString buildTargetSDK(ProjectExplorer::Target *target); static bool signPackage(ProjectExplorer::Target *target); diff --git a/src/plugins/android/androidrunnable.h b/src/plugins/android/androidrunnable.h index b9cf920285..6d6fdb8f90 100644 --- a/src/plugins/android/androidrunnable.h +++ b/src/plugins/android/androidrunnable.h @@ -42,6 +42,7 @@ struct ANDROID_EXPORT AndroidRunnable QString deviceSerialNumber; QString extraAppParams; Utils::Environment extraEnvVars; + int apiLevel = -1; QString displayName() const { return packageName; } static void *staticTypeId; diff --git a/src/plugins/android/androidrunner.cpp b/src/plugins/android/androidrunner.cpp index 9e98789c8c..b4e3d11e9a 100644 --- a/src/plugins/android/androidrunner.cpp +++ b/src/plugins/android/androidrunner.cpp @@ -28,35 +28,17 @@ #include "androiddeployqtstep.h" #include "androidconfigurations.h" -#include "androidglobal.h" #include "androidrunconfiguration.h" #include "androidmanager.h" #include "androidavdmanager.h" +#include "androidrunnerworker.h" -#include <debugger/debuggerrunconfigurationaspect.h> #include <coreplugin/messagemanager.h> #include <projectexplorer/projectexplorer.h> -#include <projectexplorer/projectexplorerconstants.h> #include <projectexplorer/projectexplorersettings.h> #include <projectexplorer/target.h> -#include <qtsupport/qtkitinformation.h> -#include <utils/qtcassert.h> -#include <utils/runextensions.h> -#include <utils/synchronousprocess.h> -#include <utils/temporaryfile.h> #include <utils/url.h> -#include <chrono> -#include <memory> -#include <QApplication> -#include <QDir> -#include <QRegExp> -#include <QTime> -#include <QTcpServer> -#include <QTcpSocket> - -using namespace std; -using namespace std::placeholders; using namespace ProjectExplorer; using namespace Utils; @@ -127,568 +109,10 @@ using namespace Utils; namespace Android { namespace Internal { -const int MIN_SOCKET_HANDSHAKE_PORT = 20001; -const int MAX_SOCKET_HANDSHAKE_PORT = 20999; -static const QString pidScript = QStringLiteral("for p in /proc/[0-9]*; " - "do cat <$p/cmdline && echo :${p##*/}; done"); -static const QString pidPollingScript = QStringLiteral("while true; do sleep 1; " - "cat /proc/%1/cmdline > /dev/null; done"); - -static const QString regExpLogcat = QStringLiteral("[0-9\\-]*" // date - "\\s+" - "[0-9\\-:.]*"// time - "\\s*" - "(\\d*)" // pid 1. capture - "\\s+" - "\\d*" // unknown - "\\s+" - "(\\w)" // message type 2. capture - "\\s+" - "(.*): " // source 3. capture - "(.*)" // message 4. capture - "[\\n\\r]*" - ); -static int APP_START_TIMEOUT = 45000; - -static bool isTimedOut(const chrono::high_resolution_clock::time_point &start, - int msecs = APP_START_TIMEOUT) -{ - bool timedOut = false; - auto end = chrono::high_resolution_clock::now(); - if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs) - timedOut = true; - return timedOut; -} - -static qint64 extractPID(const QByteArray &output, const QString &packageName) -{ - qint64 pid = -1; - foreach (auto tuple, output.split('\n')) { - tuple = tuple.simplified(); - if (!tuple.isEmpty()) { - auto parts = tuple.split(':'); - QString commandName = QString::fromLocal8Bit(parts.first()); - if (parts.length() == 2 && commandName == packageName) { - pid = parts.last().toLongLong(); - break; - } - } - } - return pid; -} - -void findProcessPID(QFutureInterface<qint64> &fi, const QString &adbPath, - QStringList selector, const QString &packageName) -{ - if (packageName.isEmpty()) - return; - - qint64 processPID = -1; - chrono::high_resolution_clock::time_point start = chrono::high_resolution_clock::now(); - do { - QThread::msleep(200); - const QByteArray out = Utils::SynchronousProcess() - .runBlocking(adbPath, selector << QStringLiteral("shell") << pidScript) - .allRawOutput(); - processPID = extractPID(out, packageName); - } while (processPID == -1 && !isTimedOut(start) && !fi.isCanceled()); - - if (!fi.isCanceled()) - fi.reportResult(processPID); -} - -static void deleter(QProcess *p) -{ - p->kill(); - p->waitForFinished(); - // Might get deleted from its own signal handler. - p->deleteLater(); -} - -class AndroidRunnerWorker : public QObject -{ - Q_OBJECT - - enum DebugHandShakeType { - PingPongFiles, - SocketHandShake - }; - -public: - AndroidRunnerWorker(RunControl *runControl, const AndroidRunnable &runnable); - ~AndroidRunnerWorker(); - - void asyncStart(); - void asyncStop(); - - void setAndroidRunnable(const AndroidRunnable &runnable); - void handleRemoteDebuggerRunning(); - -signals: - void remoteProcessStarted(Utils::Port gdbServerPort, const QUrl &qmlServer, int pid); - void remoteProcessFinished(const QString &errString = QString()); - - void remoteOutput(const QString &output); - void remoteErrorOutput(const QString &output); - -private: - void onProcessIdChanged(qint64 pid); - void logcatReadStandardError(); - void logcatReadStandardOutput(); - void adbKill(qint64 pid); - QStringList selector() const; - void forceStop(); - void findPs(); - void logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError); - bool adbShellAmNeedsQuotes(); - bool runAdb(const QStringList &args, QString *exitMessage = nullptr, int timeoutS = 10); - - // Create the processes and timer in the worker thread, for correct thread affinity - std::unique_ptr<QProcess, decltype(&deleter)> m_adbLogcatProcess; - std::unique_ptr<QProcess, decltype(&deleter)> m_psIsAlive; - QScopedPointer<QTcpSocket> m_socket; - - QByteArray m_stdoutBuffer; - QByteArray m_stderrBuffer; - - QFuture<qint64> m_pidFinder; - qint64 m_processPID = -1; - bool m_useCppDebugger = false; - QmlDebug::QmlDebugServicesPreset m_qmlDebugServices; - Utils::Port m_localGdbServerPort; // Local end of forwarded debug socket. - QUrl m_qmlServer; - QString m_pingFile; - QString m_pongFile; - QString m_gdbserverPath; - QString m_gdbserverSocket; - QString m_adb; - QRegExp m_logCatRegExp; - DebugHandShakeType m_handShakeMethod = SocketHandShake; - bool m_customPort = false; - - AndroidRunnable m_androidRunnable; - int m_socketHandShakePort = MIN_SOCKET_HANDSHAKE_PORT; -}; - -AndroidRunnerWorker::AndroidRunnerWorker(RunControl *runControl, const AndroidRunnable &runnable) - : m_adbLogcatProcess(nullptr, deleter) - , m_psIsAlive(nullptr, deleter) - , m_logCatRegExp(regExpLogcat) - , m_androidRunnable(runnable) -{ - auto runConfig = runControl->runConfiguration(); - auto aspect = runConfig->extraAspect<Debugger::DebuggerRunConfigurationAspect>(); - Core::Id runMode = runControl->runMode(); - const bool debuggingMode = runMode == ProjectExplorer::Constants::DEBUG_RUN_MODE; - m_useCppDebugger = debuggingMode && aspect->useCppDebugger(); - if (debuggingMode && aspect->useQmlDebugger()) - m_qmlDebugServices = QmlDebug::QmlDebuggerServices; - else if (runMode == ProjectExplorer::Constants::QML_PROFILER_RUN_MODE) - m_qmlDebugServices = QmlDebug::QmlProfilerServices; - else if (runMode == ProjectExplorer::Constants::QML_PREVIEW_RUN_MODE) - m_qmlDebugServices = QmlDebug::QmlPreviewServices; - else - m_qmlDebugServices = QmlDebug::NoQmlDebugServices; - m_localGdbServerPort = Utils::Port(5039); - QTC_CHECK(m_localGdbServerPort.isValid()); - if (m_qmlDebugServices != QmlDebug::NoQmlDebugServices) { - QTcpServer server; - QTC_ASSERT(server.listen(QHostAddress::LocalHost) - || server.listen(QHostAddress::LocalHostIPv6), - qDebug() << tr("No free ports available on host for QML debugging.")); - m_qmlServer.setScheme(Utils::urlTcpScheme()); - m_qmlServer.setHost(server.serverAddress().toString()); - m_qmlServer.setPort(server.serverPort()); - } - m_adb = AndroidConfigurations::currentConfig().adbToolPath().toString(); - - QString packageDir = "/data/data/" + m_androidRunnable.packageName; - m_pingFile = packageDir + "/debug-ping"; - m_pongFile = "/data/local/tmp/qt/debug-pong-" + m_androidRunnable.packageName; - m_gdbserverSocket = packageDir + "/debug-socket"; - const QtSupport::BaseQtVersion *version = QtSupport::QtKitInformation::qtVersion( - runConfig->target()->kit()); - if (version && version->qtVersion() >= QtSupport::QtVersionNumber(5, 4, 0)) - m_gdbserverPath = packageDir + "/lib/libgdbserver.so"; - else - m_gdbserverPath = packageDir + "/lib/gdbserver"; - - if (version && version->qtVersion() >= QtSupport::QtVersionNumber(5, 4, 0)) { - if (qEnvironmentVariableIsSet("QTC_ANDROID_USE_FILE_HANDSHAKE")) - m_handShakeMethod = PingPongFiles; - } else { - m_handShakeMethod = PingPongFiles; - } - - if (qEnvironmentVariableIsSet("QTC_ANDROID_SOCKET_HANDSHAKE_PORT")) { - QByteArray envData = qgetenv("QTC_ANDROID_SOCKET_HANDSHAKE_PORT"); - if (!envData.isEmpty()) { - bool ok = false; - int port = 0; - port = envData.toInt(&ok); - if (ok && port > 0 && port < 65535) { - m_socketHandShakePort = port; - m_customPort = true; - } - } - } -} - -AndroidRunnerWorker::~AndroidRunnerWorker() -{ - if (!m_pidFinder.isFinished()) - m_pidFinder.cancel(); -} - -void AndroidRunnerWorker::forceStop() -{ - runAdb({"shell", "am", "force-stop", m_androidRunnable.packageName}, nullptr, 30); - - // try killing it via kill -9 - const QByteArray out = Utils::SynchronousProcess() - .runBlocking(m_adb, selector() << QStringLiteral("shell") << pidScript) - .allRawOutput(); - - qint64 pid = extractPID(out.simplified(), m_androidRunnable.packageName); - if (pid != -1) { - adbKill(pid); - } -} - -void AndroidRunnerWorker::asyncStart() -{ - forceStop(); - - // Start the logcat process before app starts. - std::unique_ptr<QProcess, decltype(&deleter)> logcatProcess(new QProcess, deleter); - connect(logcatProcess.get(), &QProcess::readyReadStandardOutput, - this, &AndroidRunnerWorker::logcatReadStandardOutput); - connect(logcatProcess.get(), &QProcess::readyReadStandardError, - this, &AndroidRunnerWorker::logcatReadStandardError); - // Its assumed that the device or avd returned by selector() is online. - logcatProcess->start(m_adb, selector() << "logcat"); - - QString errorMessage; - - if (m_useCppDebugger) - runAdb({"shell", "rm", m_pongFile}); // Remove pong file. - - for (const QString &entry: m_androidRunnable.beforeStartAdbCommands) - runAdb(entry.split(' ', QString::SkipEmptyParts)); - - QStringList args({"shell", "am", "start"}); - args << m_androidRunnable.amStartExtraArgs; - args << "-n" << m_androidRunnable.intentName; - - if (m_useCppDebugger) { - if (!runAdb({"forward", "--remove", "tcp:" + m_localGdbServerPort.toString()})){ - QTC_CHECK(false); - } - if (!runAdb({"forward", "tcp:" + m_localGdbServerPort.toString(), - "localfilesystem:" + m_gdbserverSocket}, &errorMessage)) { - emit remoteProcessFinished(tr("Failed to forward C++ debugging ports. Reason: %1.").arg(errorMessage)); - return; - } - - const QString pingPongSocket(m_androidRunnable.packageName + ".ping_pong_socket"); - args << "-e" << "debug_ping" << "true"; - if (m_handShakeMethod == SocketHandShake) { - args << "-e" << "ping_socket" << pingPongSocket; - } else if (m_handShakeMethod == PingPongFiles) { - args << "-e" << "ping_file" << m_pingFile; - args << "-e" << "pong_file" << m_pongFile; - } - - QString gdbserverCommand = QString::fromLatin1(adbShellAmNeedsQuotes() ? "\"%1 --multi +%2\"" : "%1 --multi +%2") - .arg(m_gdbserverPath).arg(m_gdbserverSocket); - args << "-e" << "gdbserver_command" << gdbserverCommand; - args << "-e" << "gdbserver_socket" << m_gdbserverSocket; - - if (m_handShakeMethod == SocketHandShake) { - const QString port = QString("tcp:%1").arg(m_socketHandShakePort); - if (!runAdb({"forward", port, "localabstract:" + pingPongSocket}, &errorMessage)) { - emit remoteProcessFinished(tr("Failed to forward ping pong ports. Reason: %1.") - .arg(errorMessage)); - return; - } - } - } - - if (m_qmlDebugServices != QmlDebug::NoQmlDebugServices) { - // currently forward to same port on device and host - const QString port = QString("tcp:%1").arg(m_qmlServer.port()); - if (!runAdb({"forward", port, port}, &errorMessage)) { - emit remoteProcessFinished(tr("Failed to forward QML debugging ports. Reason: %1.") - .arg(errorMessage)); - return; - } - - args << "-e" << "qml_debug" << "true" - << "-e" << "qmljsdebugger" - << QString("port:%1,block,services:%2") - .arg(m_qmlServer.port()).arg(QmlDebug::qmlDebugServices(m_qmlDebugServices)); - } - - if (!m_androidRunnable.extraAppParams.isEmpty()) { - args << "-e" << "extraappparams" - << QString::fromLatin1(m_androidRunnable.extraAppParams.toUtf8().toBase64()); - } - - if (m_androidRunnable.extraEnvVars.size() > 0) { - args << "-e" << "extraenvvars" - << QString::fromLatin1(m_androidRunnable.extraEnvVars.toStringList().join('\t') - .toUtf8().toBase64()); - } - - if (!runAdb(args, &errorMessage)) { - emit remoteProcessFinished(tr("Failed to start the activity. Reason: %1.") - .arg(errorMessage)); - return; - } - - if (m_useCppDebugger) { - if (m_handShakeMethod == SocketHandShake) { - //Handling socket - bool wasSuccess = false; - const int maxAttempts = 20; //20 seconds - m_socket.reset(new QTcpSocket()); - for (int i = 0; i < maxAttempts; i++) { - - QThread::sleep(1); // give Android time to start process - m_socket->connectToHost(QHostAddress(QStringLiteral("127.0.0.1")), - m_socketHandShakePort); - if (!m_socket->waitForConnected()) - continue; - - if (!m_socket->waitForReadyRead()) { - m_socket->close(); - continue; - } - - const QByteArray pid = m_socket->readLine(); - if (pid.isEmpty()) { - m_socket->close(); - continue; - } - - wasSuccess = true; - - break; - } - - if (!m_customPort) { - // increment running port to avoid clash when using multiple - // debug sessions at the same time - m_socketHandShakePort++; - // wrap ports around to avoid overflow - if (m_socketHandShakePort == MAX_SOCKET_HANDSHAKE_PORT) - m_socketHandShakePort = MIN_SOCKET_HANDSHAKE_PORT; - } - - if (!wasSuccess) { - emit remoteProcessFinished(tr("Failed to contact debugging port.")); - return; - } - } else { - // Handling ping. - for (int i = 0; ; ++i) { - Utils::TemporaryFile tmp("pingpong"); - tmp.open(); - tmp.close(); - - runAdb({"pull", m_pingFile, tmp.fileName()}); - - QFile res(tmp.fileName()); - const bool doBreak = res.size(); - res.remove(); - if (doBreak) - break; - - if (i == 20) { - emit remoteProcessFinished(tr("Unable to start \"%1\".") - .arg(m_androidRunnable.packageName)); - return; - } - qDebug() << "WAITING FOR " << tmp.fileName(); - QThread::msleep(500); - } - } - - } - - QTC_ASSERT(!m_adbLogcatProcess, /**/); - m_adbLogcatProcess = std::move(logcatProcess); - m_pidFinder = Utils::onResultReady(Utils::runAsync(&findProcessPID, m_adb, selector(), - m_androidRunnable.packageName), - bind(&AndroidRunnerWorker::onProcessIdChanged, this, _1)); - -} - -bool AndroidRunnerWorker::adbShellAmNeedsQuotes() -{ - // Between Android SDK Tools version 24.3.1 and 24.3.4 the quoting - // needs for the 'adb shell am start ...' parameters changed. - // Run a test to find out on what side of the fence we live. - // The command will fail with a complaint about the "--dummy" - // option on newer SDKs, and with "No intent supplied" on older ones. - // In case the test itself fails assume a new SDK. - Utils::SynchronousProcess adb; - adb.setTimeoutS(10); - Utils::SynchronousProcessResponse response - = adb.run(m_adb, selector() << "shell" << "am" << "start" - << "-e" << "dummy" << "dummy --dummy"); - if (response.result == Utils::SynchronousProcessResponse::StartFailed - || response.result != Utils::SynchronousProcessResponse::Finished) - return true; - - const QString output = response.allOutput(); - const bool oldSdk = output.contains("Error: No intent supplied"); - return !oldSdk; -} - -bool AndroidRunnerWorker::runAdb(const QStringList &args, QString *exitMessage, int timeoutS) -{ - Utils::SynchronousProcess adb; - adb.setTimeoutS(timeoutS); - Utils::SynchronousProcessResponse response = adb.run(m_adb, selector() + args); - if (exitMessage) - *exitMessage = response.exitMessage(m_adb, timeoutS); - return response.result == Utils::SynchronousProcessResponse::Finished; -} - -void AndroidRunnerWorker::handleRemoteDebuggerRunning() -{ - if (m_useCppDebugger) { - if (m_handShakeMethod == SocketHandShake) { - m_socket->write("OK"); - m_socket->waitForBytesWritten(); - m_socket->close(); - } else { - Utils::TemporaryFile tmp("pingpong"); - tmp.open(); - - runAdb({"push", tmp.fileName(), m_pongFile}); - } - QTC_CHECK(m_processPID != -1); - } -// emit remoteProcessStarted(m_localGdbServerPort, m_qmlPort); -} - -void AndroidRunnerWorker::asyncStop() -{ - if (!m_pidFinder.isFinished()) - m_pidFinder.cancel(); - - if (m_processPID != -1) { - forceStop(); - } -} - -void AndroidRunnerWorker::setAndroidRunnable(const AndroidRunnable &runnable) -{ - m_androidRunnable = runnable; -} - -void AndroidRunnerWorker::logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError) -{ - QList<QByteArray> lines = text.split('\n'); - // lines always contains at least one item - lines[0].prepend(buffer); - if (!lines.last().endsWith('\n')) { - // incomplete line - buffer = lines.last(); - lines.removeLast(); - } else { - buffer.clear(); - } - - QString pidString = QString::number(m_processPID); - foreach (const QByteArray &msg, lines) { - const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n'); - if (!line.contains(pidString)) - continue; - if (m_logCatRegExp.exactMatch(line)) { - // Android M - if (m_logCatRegExp.cap(1) == pidString) { - const QString &messagetype = m_logCatRegExp.cap(2); - QString output = line.mid(m_logCatRegExp.pos(2)); - - if (onlyError - || messagetype == QLatin1String("F") - || messagetype == QLatin1String("E") - || messagetype == QLatin1String("W")) - emit remoteErrorOutput(output); - else - emit remoteOutput(output); - } - } else { - if (onlyError || line.startsWith("F/") - || line.startsWith("E/") - || line.startsWith("W/")) - emit remoteErrorOutput(line); - else - emit remoteOutput(line); - } - } -} - -void AndroidRunnerWorker::onProcessIdChanged(qint64 pid) -{ - // Don't write to m_psProc from a different thread - QTC_ASSERT(QThread::currentThread() == thread(), return); - m_processPID = pid; - if (pid == -1) { - emit remoteProcessFinished(QLatin1String("\n\n") + tr("\"%1\" died.") - .arg(m_androidRunnable.packageName)); - // App died/killed. Reset log and monitor processes. - m_adbLogcatProcess.reset(); - m_psIsAlive.reset(); - - // Run adb commands after application quit. - for (const QString &entry: m_androidRunnable.afterFinishAdbCommands) - runAdb(entry.split(' ', QString::SkipEmptyParts)); - } else { - // In debugging cases this will be funneled to the engine to actually start - // and attach gdb. Afterwards this ends up in handleRemoteDebuggerRunning() below. - emit remoteProcessStarted(m_localGdbServerPort, m_qmlServer, m_processPID); - logcatReadStandardOutput(); - QTC_ASSERT(!m_psIsAlive, /**/); - m_psIsAlive.reset(new QProcess); - m_psIsAlive->setProcessChannelMode(QProcess::MergedChannels); - connect(m_psIsAlive.get(), &QProcess::readyRead, [this](){ - if (!m_psIsAlive->readAll().simplified().isEmpty()) - onProcessIdChanged(-1); - }); - m_psIsAlive->start(m_adb, selector() << QStringLiteral("shell") - << pidPollingScript.arg(m_processPID)); - } -} - -void AndroidRunnerWorker::logcatReadStandardError() -{ - if (m_processPID != -1) - logcatProcess(m_adbLogcatProcess->readAllStandardError(), m_stderrBuffer, true); -} - -void AndroidRunnerWorker::logcatReadStandardOutput() -{ - if (m_processPID != -1) - logcatProcess(m_adbLogcatProcess->readAllStandardOutput(), m_stdoutBuffer, false); -} - -void AndroidRunnerWorker::adbKill(qint64 pid) -{ - runAdb({"shell", "kill", "-9", QString::number(pid)}); - runAdb({"shell", "run-as", m_androidRunnable.packageName, "kill", "-9", QString::number(pid)}); -} - -QStringList AndroidRunnerWorker::selector() const -{ - return AndroidDeviceInfo::adbSelector(m_androidRunnable.deviceSerialNumber); -} - -AndroidRunner::AndroidRunner(RunControl *runControl, const QString &intentName, - const QString &extraAppParams, const Utils::Environment &extraEnvVars) +AndroidRunner::AndroidRunner(RunControl *runControl, + const QString &intentName, + const QString &extraAppParams, + const Utils::Environment &extraEnvVars) : RunWorker(runControl), m_target(runControl->runConfiguration()->target()) { setDisplayName("AndroidRunner"); @@ -709,6 +133,7 @@ AndroidRunner::AndroidRunner(RunControl *runControl, const QString &intentName, m_androidRunnable.extraAppParams = extraAppParams; m_androidRunnable.extraEnvVars = extraEnvVars; m_androidRunnable.deviceSerialNumber = AndroidManager::deviceSerialNumber(m_target); + m_androidRunnable.apiLevel = AndroidManager::deviceApiLevel(m_target); if (auto androidRunConfig = qobject_cast<AndroidRunConfiguration *>( runControl->runConfiguration())) { @@ -720,23 +145,26 @@ AndroidRunner::AndroidRunner(RunControl *runControl, const QString &intentName, m_androidRunnable.afterFinishAdbCommands.append(QString("shell %1").arg(shellCmd)); } - m_worker.reset(new AndroidRunnerWorker(runControl, m_androidRunnable)); + if (m_androidRunnable.apiLevel > 23) + m_worker.reset(new AndroidRunnerWorker(runControl, m_androidRunnable)); + else + m_worker.reset(new AndroidRunnerWorkerPreNougat(runControl, m_androidRunnable)); m_worker->moveToThread(&m_thread); - connect(this, &AndroidRunner::asyncStart, m_worker.data(), &AndroidRunnerWorker::asyncStart); - connect(this, &AndroidRunner::asyncStop, m_worker.data(), &AndroidRunnerWorker::asyncStop); + connect(this, &AndroidRunner::asyncStart, m_worker.data(), &AndroidRunnerWorkerBase::asyncStart); + connect(this, &AndroidRunner::asyncStop, m_worker.data(), &AndroidRunnerWorkerBase::asyncStop); connect(this, &AndroidRunner::androidRunnableChanged, - m_worker.data(), &AndroidRunnerWorker::setAndroidRunnable); + m_worker.data(), &AndroidRunnerWorkerBase::setAndroidRunnable); connect(this, &AndroidRunner::remoteDebuggerRunning, - m_worker.data(), &AndroidRunnerWorker::handleRemoteDebuggerRunning); + m_worker.data(), &AndroidRunnerWorkerBase::handleRemoteDebuggerRunning); - connect(m_worker.data(), &AndroidRunnerWorker::remoteProcessStarted, + connect(m_worker.data(), &AndroidRunnerWorkerBase::remoteProcessStarted, this, &AndroidRunner::handleRemoteProcessStarted); - connect(m_worker.data(), &AndroidRunnerWorker::remoteProcessFinished, + connect(m_worker.data(), &AndroidRunnerWorkerBase::remoteProcessFinished, this, &AndroidRunner::handleRemoteProcessFinished); - connect(m_worker.data(), &AndroidRunnerWorker::remoteOutput, + connect(m_worker.data(), &AndroidRunnerWorkerBase::remoteOutput, this, &AndroidRunner::remoteOutput); - connect(m_worker.data(), &AndroidRunnerWorker::remoteErrorOutput, + connect(m_worker.data(), &AndroidRunnerWorkerBase::remoteErrorOutput, this, &AndroidRunner::remoteErrorOutput); connect(&m_outputParser, &QmlDebug::QmlOutputParser::waitingForConnectionOnPort, @@ -840,6 +268,7 @@ void AndroidRunner::launchAVD() m_target->project(), deviceAPILevel, targetArch); AndroidManager::setDeviceSerialNumber(m_target, info.serialNumber); m_androidRunnable.deviceSerialNumber = info.serialNumber; + m_androidRunnable.apiLevel = info.sdk; emit androidRunnableChanged(m_androidRunnable); if (info.isValid()) { AndroidAvdManager avdManager; @@ -872,5 +301,3 @@ void AndroidRunner::checkAVD() } // namespace Internal } // namespace Android - -#include "androidrunner.moc" diff --git a/src/plugins/android/androidrunner.h b/src/plugins/android/androidrunner.h index 0032fc3720..fc6b4e6470 100644 --- a/src/plugins/android/androidrunner.h +++ b/src/plugins/android/androidrunner.h @@ -43,7 +43,7 @@ namespace Android { namespace Internal { -class AndroidRunnerWorker; +class AndroidRunnerWorkerBase; class AndroidRunner : public ProjectExplorer::RunWorker { @@ -88,7 +88,7 @@ private: QString m_launchedAVDName; QThread m_thread; QTimer m_checkAVDTimer; - QScopedPointer<AndroidRunnerWorker> m_worker; + QScopedPointer<AndroidRunnerWorkerBase> m_worker; QPointer<ProjectExplorer::Target> m_target; Utils::Port m_gdbServerPort; QUrl m_qmlServer; diff --git a/src/plugins/android/androidrunnerworker.cpp b/src/plugins/android/androidrunnerworker.cpp new file mode 100644 index 0000000000..11ae7d3db8 --- /dev/null +++ b/src/plugins/android/androidrunnerworker.cpp @@ -0,0 +1,569 @@ +/**************************************************************************** +** +** Copyright (C) 2018 BogDan Vatra <bog_dan_ro@yahoo.com> +** 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 "androidconfigurations.h" +#include "androidrunnerworker.h" + +#include <QThread> + +#include <debugger/debuggerrunconfigurationaspect.h> +#include <projectexplorer/target.h> +#include <qtsupport/baseqtversion.h> +#include <qtsupport/qtkitinformation.h> +#include <utils/hostosinfo.h> +#include <utils/runextensions.h> +#include <utils/synchronousprocess.h> +#include <utils/temporaryfile.h> +#include <utils/url.h> + +#include <QTcpServer> +#include <chrono> + +using namespace std; +using namespace std::placeholders; +using namespace ProjectExplorer; + +namespace Android { +namespace Internal { + + +static const QString pidScript = "pidof -s \"%1\""; +static const QString pidScriptPreNougat = QStringLiteral("for p in /proc/[0-9]*; " + "do cat <$p/cmdline && echo :${p##*/}; done"); +static const QString pidPollingScript = QStringLiteral("while [ -d /proc/%1 ]; do sleep 1; done"); + +static const QString regExpLogcat = QStringLiteral("[0-9\\-]*" // date + "\\s+" + "[0-9\\-:.]*"// time + "\\s*" + "(\\d*)" // pid 1. capture + "\\s+" + "\\d*" // unknown + "\\s+" + "(\\w)" // message type 2. capture + "\\s+" + "(.*): " // source 3. capture + "(.*)" // message 4. capture + "[\\n\\r]*" + ); +static int APP_START_TIMEOUT = 45000; + +static bool isTimedOut(const chrono::high_resolution_clock::time_point &start, + int msecs = APP_START_TIMEOUT) +{ + bool timedOut = false; + auto end = chrono::high_resolution_clock::now(); + if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs) + timedOut = true; + return timedOut; +} + +static qint64 extractPID(const QByteArray &output, const QString &packageName) +{ + qint64 pid = -1; + foreach (auto tuple, output.split('\n')) { + tuple = tuple.simplified(); + if (!tuple.isEmpty()) { + auto parts = tuple.split(':'); + QString commandName = QString::fromLocal8Bit(parts.first()); + if (parts.length() == 2 && commandName == packageName) { + pid = parts.last().toLongLong(); + break; + } + } + } + return pid; +} + +void findProcessPIDPreNougat(QFutureInterface<qint64> &fi, const QString &adbPath, + QStringList selector, const QString &packageName) +{ + if (packageName.isEmpty()) + return; + + qint64 processPID = -1; + chrono::high_resolution_clock::time_point start = chrono::high_resolution_clock::now(); + do { + QThread::msleep(200); + const QByteArray out = Utils::SynchronousProcess() + .runBlocking(adbPath, selector << QStringLiteral("shell") << pidScriptPreNougat) + .allRawOutput(); + processPID = extractPID(out, packageName); + } while (processPID == -1 && !isTimedOut(start) && !fi.isCanceled()); + + if (!fi.isCanceled()) + fi.reportResult(processPID); +} + +void findProcessPID(QFutureInterface<qint64> &fi, const QString &adbPath, + QStringList selector, const QString &packageName) +{ + if (packageName.isEmpty()) + return; + + qint64 processPID = -1; + chrono::high_resolution_clock::time_point start = chrono::high_resolution_clock::now(); + do { + QThread::msleep(200); + const QByteArray out = Utils::SynchronousProcess() + .runBlocking(adbPath, selector << QStringLiteral("shell") << pidScript.arg(packageName)) + .allRawOutput(); + if (!out.isEmpty()) + processPID = out.trimmed().toLongLong(); + } while (processPID == -1 && !isTimedOut(start) && !fi.isCanceled()); + + if (!fi.isCanceled()) + fi.reportResult(processPID); +} + +AndroidRunnerWorkerBase::AndroidRunnerWorkerBase(RunControl *runControl, const AndroidRunnable &runnable) + : m_androidRunnable(runnable) + , m_adbLogcatProcess(nullptr, deleter) + , m_psIsAlive(nullptr, deleter) + , m_logCatRegExp(regExpLogcat) + , m_gdbServerProcess(nullptr, deleter) + , m_jdbProcess(nullptr, deleter) + +{ + auto runConfig = runControl->runConfiguration(); + auto aspect = runConfig->extraAspect<Debugger::DebuggerRunConfigurationAspect>(); + Core::Id runMode = runControl->runMode(); + const bool debuggingMode = runMode == ProjectExplorer::Constants::DEBUG_RUN_MODE; + m_useCppDebugger = debuggingMode && aspect->useCppDebugger(); + if (debuggingMode && aspect->useQmlDebugger()) + m_qmlDebugServices = QmlDebug::QmlDebuggerServices; + else if (runMode == ProjectExplorer::Constants::QML_PROFILER_RUN_MODE) + m_qmlDebugServices = QmlDebug::QmlProfilerServices; + else if (runMode == ProjectExplorer::Constants::QML_PREVIEW_RUN_MODE) + m_qmlDebugServices = QmlDebug::QmlPreviewServices; + else + m_qmlDebugServices = QmlDebug::NoQmlDebugServices; + m_localGdbServerPort = Utils::Port(5039); + QTC_CHECK(m_localGdbServerPort.isValid()); + if (m_qmlDebugServices != QmlDebug::NoQmlDebugServices) { + QTcpServer server; + QTC_ASSERT(server.listen(QHostAddress::LocalHost) + || server.listen(QHostAddress::LocalHostIPv6), + qDebug() << tr("No free ports available on host for QML debugging.")); + m_qmlServer.setScheme(Utils::urlTcpScheme()); + m_qmlServer.setHost(server.serverAddress().toString()); + m_qmlServer.setPort(server.serverPort()); + } + m_adb = AndroidConfigurations::currentConfig().adbToolPath().toString(); + m_localJdbServerPort = Utils::Port(5038); + QTC_CHECK(m_localJdbServerPort.isValid()); +} + +AndroidRunnerWorkerBase::~AndroidRunnerWorkerBase() +{ + if (m_processPID != -1) + forceStop(); + + if (!m_pidFinder.isFinished()) + m_pidFinder.cancel(); +} + +bool AndroidRunnerWorkerBase::adbShellAmNeedsQuotes() +{ + // Between Android SDK Tools version 24.3.1 and 24.3.4 the quoting + // needs for the 'adb shell am start ...' parameters changed. + // Run a test to find out on what side of the fence we live. + // The command will fail with a complaint about the "--dummy" + // option on newer SDKs, and with "No intent supplied" on older ones. + // In case the test itself fails assume a new SDK. + Utils::SynchronousProcess adb; + adb.setTimeoutS(10); + Utils::SynchronousProcessResponse response + = adb.run(m_adb, selector() << "shell" << "am" << "start" + << "-e" << "dummy" << "dummy --dummy"); + if (response.result == Utils::SynchronousProcessResponse::StartFailed + || response.result != Utils::SynchronousProcessResponse::Finished) + return true; + + const QString output = response.allOutput(); + const bool oldSdk = output.contains("Error: No intent supplied"); + return !oldSdk; +} + +bool AndroidRunnerWorkerBase::runAdb(const QStringList &args, int timeoutS) +{ + Utils::SynchronousProcess adb; + adb.setTimeoutS(timeoutS); + Utils::SynchronousProcessResponse response = adb.run(m_adb, selector() + args); + m_lastRunAdbError = response.exitMessage(m_adb, timeoutS); + m_lastRunAdbRawOutput = response.allRawOutput(); + return response.result == Utils::SynchronousProcessResponse::Finished; +} + +void AndroidRunnerWorkerBase::adbKill(qint64 pid) +{ + runAdb({"shell", "kill", "-9", QString::number(pid)}); + runAdb({"shell", "run-as", m_androidRunnable.packageName, "kill", "-9", QString::number(pid)}); +} + + +QStringList AndroidRunnerWorkerBase::selector() const +{ + return AndroidDeviceInfo::adbSelector(m_androidRunnable.deviceSerialNumber); +} + +void AndroidRunnerWorkerBase::forceStop() +{ + runAdb({"shell", "am", "force-stop", m_androidRunnable.packageName}, 30); + + // try killing it via kill -9 + const QByteArray out = Utils::SynchronousProcess() + .runBlocking(m_adb, selector() << QStringLiteral("shell") << pidScriptPreNougat) + .allRawOutput(); + + qint64 pid = extractPID(out.simplified(), m_androidRunnable.packageName); + if (pid != -1) { + adbKill(pid); + } +} + +void AndroidRunnerWorkerBase::logcatReadStandardError() +{ + if (m_processPID != -1) + logcatProcess(m_adbLogcatProcess->readAllStandardError(), m_stderrBuffer, true); +} + +void AndroidRunnerWorkerBase::logcatReadStandardOutput() +{ + if (m_processPID != -1) + logcatProcess(m_adbLogcatProcess->readAllStandardOutput(), m_stdoutBuffer, false); +} + +void AndroidRunnerWorkerBase::logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError) +{ + QList<QByteArray> lines = text.split('\n'); + // lines always contains at least one item + lines[0].prepend(buffer); + if (!lines.last().endsWith('\n')) { + // incomplete line + buffer = lines.last(); + lines.removeLast(); + } else { + buffer.clear(); + } + + QString pidString = QString::number(m_processPID); + foreach (const QByteArray &msg, lines) { + const QString line = QString::fromUtf8(msg).trimmed() + QLatin1Char('\n'); + if (!line.contains(pidString)) + continue; + if (m_useCppDebugger) { + switch (m_jdbState) { + case JDBState::Idle: + if (msg.trimmed().endsWith("Sending WAIT chunk")) { + m_jdbState = JDBState::Waiting; + handleJdbWaiting(); + } + break; + case JDBState::Waiting: + if (msg.indexOf("debugger has settled") > 0) { + m_jdbState = JDBState::Settled; + handleJdbSettled(); + } + break; + default: + break; + } + } + if (m_logCatRegExp.exactMatch(line)) { + // Android M + if (m_logCatRegExp.cap(1) == pidString) { + const QString &messagetype = m_logCatRegExp.cap(2); + QString output = line.mid(m_logCatRegExp.pos(2)); + + if (onlyError + || messagetype == QLatin1String("F") + || messagetype == QLatin1String("E") + || messagetype == QLatin1String("W")) + emit remoteErrorOutput(output); + else + emit remoteOutput(output); + } + } else { + if (onlyError || line.startsWith("F/") + || line.startsWith("E/") + || line.startsWith("W/")) + emit remoteErrorOutput(line); + else + emit remoteOutput(line); + } + } +} + +void AndroidRunnerWorkerBase::setAndroidRunnable(const AndroidRunnable &runnable) +{ + m_androidRunnable = runnable; +} + +void AndroidRunnerWorkerBase::asyncStart() +{ + forceStop(); + + // Start the logcat process before app starts. + std::unique_ptr<QProcess, decltype(&deleter)> logcatProcess(new QProcess, deleter); + connect(logcatProcess.get(), &QProcess::readyReadStandardOutput, + this, &AndroidRunnerWorkerBase::logcatReadStandardOutput); + connect(logcatProcess.get(), &QProcess::readyReadStandardError, + this, &AndroidRunnerWorkerBase::logcatReadStandardError); + // Its assumed that the device or avd returned by selector() is online. + logcatProcess->start(m_adb, selector() << "logcat"); + QTC_ASSERT(!m_adbLogcatProcess, /**/); + m_adbLogcatProcess = std::move(logcatProcess); + + for (const QString &entry: m_androidRunnable.beforeStartAdbCommands) + runAdb(entry.split(' ', QString::SkipEmptyParts)); + + QStringList args({"shell", "am", "start"}); + args << m_androidRunnable.amStartExtraArgs; + args << "-n" << m_androidRunnable.intentName; + if (m_useCppDebugger) { + args << "-D"; + QString gdbServerSocket; + // run-as <package-name> pwd fails on API 22 so route the pwd through shell. + if (!runAdb({"shell", "run-as", m_androidRunnable.packageName, "/system/bin/sh", "-c", "pwd"})) { + emit remoteProcessFinished(tr("Failed to get process path. Reason: %1.").arg(m_lastRunAdbError)); + return; + } + gdbServerSocket = QString::fromUtf8(m_lastRunAdbRawOutput.trimmed()) + "/debug-socket"; + + QString gdbServerExecutable; + if (!runAdb({"shell", "run-as", m_androidRunnable.packageName, "ls", "lib/"})) { + emit remoteProcessFinished(tr("Failed to get process path. Reason: %1.").arg(m_lastRunAdbError)); + return; + } + + for (const auto &line: m_lastRunAdbRawOutput.split('\n')) { + if (line.indexOf("gdbserver") != -1/* || line.indexOf("lldb-server") != -1*/) { + gdbServerExecutable = QString::fromUtf8(line.trimmed()); + break; + } + } + + if (gdbServerExecutable.isEmpty()) { + emit remoteProcessFinished(tr("Can't find C++ debugger.")); + return; + } + + runAdb({"shell", "run-as", m_androidRunnable.packageName, "killall", gdbServerExecutable}); + runAdb({"shell", "run-as", m_androidRunnable.packageName, "rm", gdbServerSocket}); + std::unique_ptr<QProcess, decltype(&deleter)> gdbServerProcess(new QProcess, deleter); + gdbServerProcess->start(m_adb, selector() << "shell" << "run-as" + << m_androidRunnable.packageName << "lib/" + gdbServerExecutable + << "--multi" << "+" + gdbServerSocket); + if (!gdbServerProcess->waitForStarted()) { + emit remoteProcessFinished(tr("Failed to start C++ debugger.")); + return; + } + m_gdbServerProcess = std::move(gdbServerProcess); + QStringList removeForward{"forward", "--remove", "tcp:" + m_localGdbServerPort.toString()}; + runAdb(removeForward); + if (!runAdb({"forward", "tcp:" + m_localGdbServerPort.toString(), + "localfilesystem:" + gdbServerSocket})) { + emit remoteProcessFinished(tr("Failed to forward C++ debugging ports. Reason: %1.").arg(m_lastRunAdbError)); + return; + } + m_androidRunnable.afterFinishAdbCommands.push_back(removeForward.join(' ')); + } + + if (m_qmlDebugServices != QmlDebug::NoQmlDebugServices) { + // currently forward to same port on device and host + const QString port = QString("tcp:%1").arg(m_qmlServer.port()); + QStringList removeForward{{"forward", "--remove", port}}; + runAdb(removeForward); + if (!runAdb({"forward", port, port})) { + emit remoteProcessFinished(tr("Failed to forward QML debugging ports. Reason: %1.") + .arg(m_lastRunAdbError)); + return; + } + m_androidRunnable.afterFinishAdbCommands.push_back(removeForward.join(' ')); + + args << "-e" << "qml_debug" << "true" + << "-e" << "qmljsdebugger" + << QString("port:%1,block,services:%2") + .arg(m_qmlServer.port()).arg(QmlDebug::qmlDebugServices(m_qmlDebugServices)); + } + + if (!m_androidRunnable.extraAppParams.isEmpty()) { + args << "-e" << "extraappparams" + << QString::fromLatin1(m_androidRunnable.extraAppParams.toUtf8().toBase64()); + } + + if (m_androidRunnable.extraEnvVars.size() > 0) { + args << "-e" << "extraenvvars" + << QString::fromLatin1(m_androidRunnable.extraEnvVars.toStringList().join('\t') + .toUtf8().toBase64()); + } + + if (!runAdb(args)) { + emit remoteProcessFinished(tr("Failed to start the activity. Reason: %1.") + .arg(m_lastRunAdbError)); + return; + } +} + +void AndroidRunnerWorkerBase::asyncStop() +{ + if (!m_pidFinder.isFinished()) + m_pidFinder.cancel(); + + if (m_processPID != -1) + forceStop(); + + m_jdbProcess.reset(); + m_gdbServerProcess.reset(); +} + +void AndroidRunnerWorkerBase::handleRemoteDebuggerRunning() +{} + +void AndroidRunnerWorkerBase::handleJdbWaiting() +{ + QStringList removeForward{"forward", "--remove", "tcp:" + m_localJdbServerPort.toString()}; + runAdb(removeForward); + if (!runAdb({"forward", "tcp:" + m_localJdbServerPort.toString(), + "jdwp:" + QString::number(m_processPID)})) { + emit remoteProcessFinished(tr("Failed to forward jdb debugging ports. Reason: %1.").arg(m_lastRunAdbError)); + return; + } + m_androidRunnable.afterFinishAdbCommands.push_back(removeForward.join(' ')); + + auto jdbPath = AndroidConfigurations::currentConfig().openJDKLocation().appendPath("bin"); + if (Utils::HostOsInfo::isWindowsHost()) + jdbPath.appendPath("jdb.exe"); + else + jdbPath.appendPath("jdb"); + + std::unique_ptr<QProcess, decltype(&deleter)> jdbProcess(new QProcess, deleter); + jdbProcess->setProcessChannelMode(QProcess::MergedChannels); + jdbProcess->start(jdbPath.toString(), QStringList() << "-connect" << + QString("com.sun.jdi.SocketAttach:hostname=localhost,port=%1") + .arg(m_localJdbServerPort.toString())); + if (!jdbProcess->waitForStarted()) { + emit remoteProcessFinished(tr("Failed to start JDB")); + return; + } + m_jdbProcess = std::move(jdbProcess); +} + +void AndroidRunnerWorkerBase::handleJdbSettled() +{ + auto waitForCommand = [&]() { + for (int i= 0; i < 5 && m_jdbProcess->state() == QProcess::Running; ++i) { + m_jdbProcess->waitForReadyRead(500); + QByteArray lines = m_jdbProcess->readAll(); + for (const auto &line: lines.split('\n')) { + auto msg = line.trimmed(); + if (msg.startsWith(">")) + return true; + } + } + return false; + }; + if (waitForCommand()) { + m_jdbProcess->write("cont\n"); + if (m_jdbProcess->waitForBytesWritten(5000) && waitForCommand()) { + m_jdbProcess->write("exit\n"); + m_jdbProcess->waitForBytesWritten(5000); + if (!m_jdbProcess->waitForFinished(5000)) { + m_jdbProcess->terminate(); + if (!m_jdbProcess->waitForFinished(5000)) { + m_jdbProcess->kill(); + m_jdbProcess->waitForFinished(); + } + } else if (m_jdbProcess->exitStatus() == QProcess::NormalExit && m_jdbProcess->exitCode() == 0) { + return; + } + } + } + emit remoteProcessFinished(tr("Can't attach jdb to the running application").arg(m_lastRunAdbError)); +} + +void AndroidRunnerWorkerBase::onProcessIdChanged(qint64 pid) +{ + // Don't write to m_psProc from a different thread + QTC_ASSERT(QThread::currentThread() == thread(), return); + m_processPID = pid; + if (pid == -1) { + emit remoteProcessFinished(QLatin1String("\n\n") + tr("\"%1\" died.") + .arg(m_androidRunnable.packageName)); + // App died/killed. Reset log, monitor, jdb & gdb processes. + m_adbLogcatProcess.reset(); + m_psIsAlive.reset(); + m_jdbProcess.reset(); + m_gdbServerProcess.reset(); + + // Run adb commands after application quit. + for (const QString &entry: m_androidRunnable.afterFinishAdbCommands) + runAdb(entry.split(' ', QString::SkipEmptyParts)); + } else { + // In debugging cases this will be funneled to the engine to actually start + // and attach gdb. Afterwards this ends up in handleRemoteDebuggerRunning() below. + emit remoteProcessStarted(m_localGdbServerPort, m_qmlServer, m_processPID); + logcatReadStandardOutput(); + QTC_ASSERT(!m_psIsAlive, /**/); + m_psIsAlive.reset(new QProcess); + m_psIsAlive->setProcessChannelMode(QProcess::MergedChannels); + connect(m_psIsAlive.get(), static_cast<void(QProcess::*)(int)>(&QProcess::finished), + this, bind(&AndroidRunnerWorkerBase::onProcessIdChanged, this, -1)); + m_psIsAlive->start(m_adb, selector() << QStringLiteral("shell") + << pidPollingScript.arg(m_processPID)); + } +} + + +AndroidRunnerWorker::AndroidRunnerWorker(RunControl *runControl, const AndroidRunnable &runnable) + : AndroidRunnerWorkerBase(runControl, runnable) +{ +} + +void AndroidRunnerWorker::asyncStart() +{ + AndroidRunnerWorkerBase::asyncStart(); + m_pidFinder = Utils::onResultReady(Utils::runAsync(&findProcessPID, m_adb, selector(), + m_androidRunnable.packageName), + bind(&AndroidRunnerWorker::onProcessIdChanged, this, _1)); +} + +AndroidRunnerWorkerPreNougat::AndroidRunnerWorkerPreNougat(RunControl *runControl, const AndroidRunnable &runnable) + : AndroidRunnerWorkerBase(runControl, runnable) +{ +} + +void AndroidRunnerWorkerPreNougat::asyncStart() +{ + AndroidRunnerWorkerBase::asyncStart(); + m_pidFinder = Utils::onResultReady(Utils::runAsync(&findProcessPIDPreNougat, m_adb, selector(), + m_androidRunnable.packageName), + bind(&AndroidRunnerWorkerPreNougat::onProcessIdChanged, this, _1)); + +} + +} // namespace Internal +} // namespace Android diff --git a/src/plugins/android/androidrunnerworker.h b/src/plugins/android/androidrunnerworker.h new file mode 100644 index 0000000000..7a5c6614b8 --- /dev/null +++ b/src/plugins/android/androidrunnerworker.h @@ -0,0 +1,134 @@ +/**************************************************************************** +** +** Copyright (C) 2018 BogDan Vatra <bog_dan_ro@yahoo.com> +** 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. +** +****************************************************************************/ + + +#pragma once + +#include <qmldebug/qmldebugcommandlinearguments.h> + +#include <QFuture> +#include <QTcpSocket> + +#include "androidrunnable.h" + +namespace ProjectExplorer { +class RunControl; +} + +namespace Android { +namespace Internal { + +const int MIN_SOCKET_HANDSHAKE_PORT = 20001; + +static void deleter(QProcess *p) +{ + p->terminate(); + if (!p->waitForFinished(1000)) { + p->kill(); + p->waitForFinished(); + } + // Might get deleted from its own signal handler. + p->deleteLater(); +} + +class AndroidRunnerWorkerBase : public QObject +{ + Q_OBJECT +public: + AndroidRunnerWorkerBase(ProjectExplorer::RunControl *runControl, const AndroidRunnable &runnable); + ~AndroidRunnerWorkerBase() override; + bool adbShellAmNeedsQuotes(); + bool runAdb(const QStringList &args, int timeoutS = 10); + void adbKill(qint64 pid); + QStringList selector() const; + void forceStop(); + void logcatReadStandardError(); + void logcatReadStandardOutput(); + void logcatProcess(const QByteArray &text, QByteArray &buffer, bool onlyError); + void setAndroidRunnable(const AndroidRunnable &runnable); + + virtual void asyncStart(); + virtual void asyncStop(); + virtual void handleRemoteDebuggerRunning(); + virtual void handleJdbWaiting(); + virtual void handleJdbSettled(); + +signals: + void remoteProcessStarted(Utils::Port gdbServerPort, const QUrl &qmlServer, int pid); + void remoteProcessFinished(const QString &errString = QString()); + + void remoteOutput(const QString &output); + void remoteErrorOutput(const QString &output); + +protected: + enum class JDBState { + Idle, + Waiting, + Settled + }; + virtual void onProcessIdChanged(qint64 pid); + + // Create the processes and timer in the worker thread, for correct thread affinity + AndroidRunnable m_androidRunnable; + QString m_adb; + qint64 m_processPID = -1; + std::unique_ptr<QProcess, decltype(&deleter)> m_adbLogcatProcess; + std::unique_ptr<QProcess, decltype(&deleter)> m_psIsAlive; + QByteArray m_stdoutBuffer; + QByteArray m_stderrBuffer; + QRegExp m_logCatRegExp; + QFuture<qint64> m_pidFinder; + bool m_useCppDebugger = false; + QmlDebug::QmlDebugServicesPreset m_qmlDebugServices; + Utils::Port m_localGdbServerPort; // Local end of forwarded debug socket. + QUrl m_qmlServer; + QByteArray m_lastRunAdbRawOutput; + QString m_lastRunAdbError; + JDBState m_jdbState = JDBState::Idle; + Utils::Port m_localJdbServerPort; + std::unique_ptr<QProcess, decltype(&deleter)> m_gdbServerProcess; + std::unique_ptr<QProcess, decltype(&deleter)> m_jdbProcess; +}; + + +class AndroidRunnerWorker : public AndroidRunnerWorkerBase +{ + Q_OBJECT +public: + AndroidRunnerWorker(ProjectExplorer::RunControl *runControl, const AndroidRunnable &runnable); + void asyncStart() override; +}; + + +class AndroidRunnerWorkerPreNougat : public AndroidRunnerWorkerBase +{ + Q_OBJECT +public: + AndroidRunnerWorkerPreNougat(ProjectExplorer::RunControl *runControl, const AndroidRunnable &runnable); + void asyncStart() override; +}; + +} // namespace Internal +} // namespace Android |