From 2d480f45da6b5415d61c952076370be9ce8968af Mon Sep 17 00:00:00 2001 From: Jarek Kobus Date: Wed, 26 Jun 2024 14:51:27 +0200 Subject: [PATCH] Android: Use task tree for startAvd() action As a drive-by, lower some timeouts from 2s into 1s. Change-Id: I495ad20058b0f97b6cdd5271a390b779c66b4864 Reviewed-by: Alessandro Portale --- src/plugins/android/androidavdmanager.cpp | 173 ++++++++++++++++++ src/plugins/android/androidavdmanager.h | 10 + src/plugins/android/androidconfigurations.cpp | 12 ++ src/plugins/android/androidconfigurations.h | 3 + src/plugins/android/androiddevice.cpp | 44 ++--- src/plugins/android/androiddevice.h | 9 +- 6 files changed, 224 insertions(+), 27 deletions(-) diff --git a/src/plugins/android/androidavdmanager.cpp b/src/plugins/android/androidavdmanager.cpp index d05c4e0bb51..b154ea0252f 100644 --- a/src/plugins/android/androidavdmanager.cpp +++ b/src/plugins/android/androidavdmanager.cpp @@ -7,12 +7,17 @@ #include +#include +#include + +#include #include #include #include #include +using namespace Tasking; using namespace Utils; using namespace std::chrono_literals; @@ -27,6 +32,7 @@ QString startAvd(const QString &name, const std::optional> &future return {}; } +// TODO: Make async and move out of startAvdImpl, make it a part of startAvdRecipe. static bool is32BitUserSpace() { // Do a similar check as android's emulator is doing: @@ -73,6 +79,44 @@ bool startAvdAsync(const QString &avdName) return false; } +static void startAvdDetached(QPromise &promise, const CommandLine &avdCommand) +{ + qCDebug(avdManagerLog).noquote() << "Running command (startAvdDetached):" << avdCommand.toUserOutput(); + if (!Process::startDetached(avdCommand, {}, DetachedChannelMode::Discard)) + promise.future().cancel(); +} + +static CommandLine avdCommand(const QString &avdName) +{ + CommandLine cmd(AndroidConfig::emulatorToolPath()); + if (is32BitUserSpace()) + cmd.addArg("-force-32bit"); + cmd.addArgs(AndroidConfig::emulatorArgs(), CommandLine::Raw); + cmd.addArgs({"-avd", avdName}); + return cmd; +} + +ExecutableItem startAvdAsyncRecipe(const QString &avdName) +{ + const auto onSetup = [avdName](Async &async) { + const FilePath emulatorPath = AndroidConfig::emulatorToolPath(); + if (!emulatorPath.exists()) { + QMessageBox::critical(Core::ICore::dialogParent(), Tr::tr("Emulator Tool Is Missing"), + Tr::tr("Install the missing emulator tool (%1) to the " + "installed Android SDK.").arg(emulatorPath.displayName())); + return SetupResult::StopWithError; + } + async.setConcurrentCallData(startAvdDetached, avdCommand(avdName)); + return SetupResult::Continue; + }; + const auto onDone = [avdName] { + QMessageBox::critical(Core::ICore::dialogParent(), Tr::tr("AVD Start Error"), + Tr::tr("Failed to start AVD emulator for \"%1\" device.").arg(avdName)); + }; + + return AsyncTask(onSetup, onDone, CallDoneIf::Error); +} + QString findAvd(const QString &avdName) { const QStringList lines = AndroidConfig::devicesCommandOutput(); @@ -91,6 +135,69 @@ QString findAvd(const QString &avdName) return {}; } +ExecutableItem serialNumberRecipe(const QString &avdName, const Storage &serialNumberStorage) +{ + const Storage outputStorage; + const Storage currentSerialNumberStorage; + const LoopUntil iterator([outputStorage](int iteration) { return iteration < outputStorage->size(); }); + + const auto onSocketSetup = [iterator, outputStorage, currentSerialNumberStorage](TcpSocket &socket) { + const QString line = outputStorage->at(iterator.iteration()); + if (line.startsWith("* daemon")) + return SetupResult::StopWithError; + + const QString serialNumber = line.left(line.indexOf('\t')).trimmed(); + if (!serialNumber.startsWith("emulator")) + return SetupResult::StopWithError; + + const int index = serialNumber.indexOf(QLatin1String("-")); + if (index == -1) + return SetupResult::StopWithError; + + bool ok; + const int port = serialNumber.mid(index + 1).toInt(&ok); + if (!ok) + return SetupResult::StopWithError; + + *currentSerialNumberStorage = serialNumber; + + socket.setAddress(QHostAddress(QHostAddress::LocalHost)); + socket.setPort(port); + socket.setWriteData("avd name\nexit\n"); + return SetupResult::Continue; + }; + const auto onSocketDone = [avdName, currentSerialNumberStorage, serialNumberStorage](const TcpSocket &socket) { + const QByteArrayList response = socket.socket()->readAll().split('\n'); + // The input "avd name" might not be echoed as-is, but contain ASCII control sequences. + for (int i = response.size() - 1; i > 1; --i) { + if (!response.at(i).startsWith("OK")) + continue; + + const QString currentAvdName = QString::fromLatin1(response.at(i - 1)).trimmed(); + if (avdName != currentAvdName) + break; + + *serialNumberStorage = *currentSerialNumberStorage; + return DoneResult::Success; + } + return DoneResult::Error; + }; + + return Group { + outputStorage, + AndroidConfig::devicesCommandOutputRecipe(outputStorage), + For { + iterator, + parallel, + stopOnSuccess, + Group { + currentSerialNumberStorage, + TcpSocketTask(onSocketSetup, onSocketDone) + } + } + }; +} + static bool waitForBooted(const QString &serialNumber, const std::optional> &future) { // found a serial number, now wait until it's done booting... @@ -122,6 +229,72 @@ QString waitForAvd(const QString &avdName, const std::optional> &f return {}; } +static ExecutableItem isAvdBootedRecipe(const Storage &serialNumberStorage) +{ + const auto onSetup = [serialNumberStorage](Process &process) { + const CommandLine cmd{AndroidConfig::adbToolPath(), + {AndroidDeviceInfo::adbSelector(*serialNumberStorage), + "shell", "getprop", "init.svc.bootanim"}}; + qCDebug(avdManagerLog).noquote() << "Running command (isAvdBooted):" << cmd.toUserOutput(); + process.setCommand(cmd); + }; + const auto onDone = [](const Process &process, DoneWith result) { + return result == DoneWith::Success && process.allOutput().trimmed() == "stopped"; + }; + return ProcessTask(onSetup, onDone); +} + +ExecutableItem waitForAvdRecipe(const QString &avdName, const Storage &serialNumberStorage) +{ + const Storage outputStorage; + const Storage stopStorage; + + const auto onIsConnectedDone = [stopStorage, outputStorage, serialNumberStorage] { + const QString serialNumber = *serialNumberStorage; + for (const QString &line : *outputStorage) { + // skip the daemon logs + if (!line.startsWith("* daemon") && line.left(line.indexOf('\t')).trimmed() == serialNumber) + return DoneResult::Error; + } + serialNumberStorage->clear(); + *stopStorage = true; + return DoneResult::Success; + }; + + const auto onWaitForBootedDone = [stopStorage] { return !*stopStorage; }; + + return Group { + Forever { + stopOnSuccess, + serialNumberRecipe(avdName, serialNumberStorage), + TimeoutTask([](std::chrono::milliseconds &timeout) { timeout = 1s; }, DoneResult::Error) + }.withTimeout(120s), + Forever { + stopStorage, + stopOnSuccess, + isAvdBootedRecipe(serialNumberStorage), + TimeoutTask([](std::chrono::milliseconds &timeout) { timeout = 1s; }, DoneResult::Error), + Group { + outputStorage, + AndroidConfig::devicesCommandOutputRecipe(outputStorage), + onGroupDone(onIsConnectedDone, CallDoneIf::Success) + }, + onGroupDone(onWaitForBootedDone) + }.withTimeout(120s) + }; +} + +ExecutableItem startAvdRecipe(const QString &avdName, const Storage &serialNumberStorage) +{ + return Group { + If (serialNumberRecipe(avdName, serialNumberStorage) || startAvdAsyncRecipe(avdName)) >> Then { + waitForAvdRecipe(avdName, serialNumberStorage) + } >> Else { + errorItem + } + }; +} + bool isAvdBooted(const QString &device) { const CommandLine cmd{AndroidConfig::adbToolPath(), {AndroidDeviceInfo::adbSelector(device), diff --git a/src/plugins/android/androidavdmanager.h b/src/plugins/android/androidavdmanager.h index 7f1b094bdd2..23357500b05 100644 --- a/src/plugins/android/androidavdmanager.h +++ b/src/plugins/android/androidavdmanager.h @@ -6,6 +6,8 @@ #include +#include + namespace Android::Internal::AndroidAvdManager { QString startAvd(const QString &name, const std::optional> &future = {}); @@ -14,4 +16,12 @@ QString findAvd(const QString &avdName); QString waitForAvd(const QString &avdName, const std::optional> &future = {}); bool isAvdBooted(const QString &device); +Tasking::ExecutableItem serialNumberRecipe( + const QString &avdName, const Tasking::Storage &serialNumberStorage); +Tasking::ExecutableItem startAvdAsyncRecipe(const QString &avdName); +Tasking::ExecutableItem waitForAvdRecipe( + const QString &avdName, const Tasking::Storage &serialNumberStorage); +Tasking::ExecutableItem startAvdRecipe( + const QString &avdName, const Tasking::Storage &serialNumberStorage); + } // namespace Android::Internal::AndroidAvdManager diff --git a/src/plugins/android/androidconfigurations.cpp b/src/plugins/android/androidconfigurations.cpp index feadec6111e..d147a4ca1b4 100644 --- a/src/plugins/android/androidconfigurations.cpp +++ b/src/plugins/android/androidconfigurations.cpp @@ -61,6 +61,7 @@ using namespace ProjectExplorer; using namespace QtSupport; +using namespace Tasking; using namespace Utils; namespace { @@ -687,6 +688,17 @@ QStringList devicesCommandOutput() return adbProcess.allOutput().split('\n', Qt::SkipEmptyParts).mid(1); } +ExecutableItem devicesCommandOutputRecipe(const Storage &outputStorage) +{ + const auto onSetup = [](Process &process) { process.setCommand({adbToolPath(), {"devices"}}); }; + const auto onDone = [outputStorage](const Process &process) { + // mid(1) - remove "List of devices attached" header line. + // Example output: "List of devices attached\nemulator-5554\tdevice\n\n". + *outputStorage = process.allOutput().split('\n', Qt::SkipEmptyParts).mid(1); + }; + return ProcessTask(onSetup, onDone); +} + bool isConnected(const QString &serialNumber) { const QStringList lines = devicesCommandOutput(); diff --git a/src/plugins/android/androidconfigurations.h b/src/plugins/android/androidconfigurations.h index 81477b408a8..735a6e3a738 100644 --- a/src/plugins/android/androidconfigurations.h +++ b/src/plugins/android/androidconfigurations.h @@ -12,6 +12,8 @@ #include +#include + #include #include @@ -90,6 +92,7 @@ Utils::FilePath makePathFromNdk(const Utils::FilePath &ndkLocation); Utils::FilePath keytoolPath(); QStringList devicesCommandOutput(); +Tasking::ExecutableItem devicesCommandOutputRecipe(const Tasking::Storage &outputStorage); QString bestNdkPlatformMatch(int target, const QtSupport::QtVersion *qtVersion); diff --git a/src/plugins/android/androiddevice.cpp b/src/plugins/android/androiddevice.cpp index ffce38534f8..cc824e1343a 100644 --- a/src/plugins/android/androiddevice.cpp +++ b/src/plugins/android/androiddevice.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -23,13 +24,12 @@ #include #include -#include - -#include +#include #include #include #include +#include #include #include #include @@ -105,22 +105,6 @@ static void updateDeviceState(const IDevice::ConstPtr &device) devMgr->setDeviceState(id, IDevice::DeviceConnected); } -static void startAvd(const IDevice::Ptr &device, QWidget *parent) -{ - Q_UNUSED(parent) - const AndroidDevice *androidDev = static_cast(device.get()); - const QString name = androidDev->avdName(); - qCDebug(androidDeviceLog, "Starting Android AVD id \"%s\".", qPrintable(name)); - Utils::futureSynchronizer()->addFuture(Utils::asyncRun([name, device](QPromise &promise) { - const QString serialNumber = AndroidAvdManager::startAvd(name, promise.future()); - // Mark the AVD as ReadyToUse once we know it's started - if (!serialNumber.isEmpty()) { - DeviceManager *const devMgr = DeviceManager::instance(); - devMgr->setDeviceState(device->id(), IDevice::DeviceReadyToUse); - } - })); -} - static void setEmulatorArguments(QWidget *parent) { const QString helpUrl = @@ -401,8 +385,8 @@ void AndroidDevice::addActionsIfNotFound() if (machineType() == Emulator) { if (!hasStartAction) { - addDeviceAction({startAvdAction, [](const IDevice::Ptr &device, QWidget *parent) { - startAvd(device, parent); + addDeviceAction({startAvdAction, [](const IDevice::Ptr &device, QWidget *) { + static_cast(device.get())->startAvd(); }}); } @@ -581,6 +565,24 @@ QString AndroidDevice::openGLStatus() const return openGL.isEmpty() ? Tr::tr("Unknown") : openGL; } +void AndroidDevice::startAvd() +{ + const Storage serialNumberStorage; + + const auto onDone = [this, serialNumberStorage] { + if (!serialNumberStorage->isEmpty()) + DeviceManager::instance()->setDeviceState(id(), IDevice::DeviceReadyToUse); + }; + + const Group root { + serialNumberStorage, + AndroidAvdManager::startAvdRecipe(avdName(), serialNumberStorage), + onGroupDone(onDone, CallDoneIf::Success) + }; + + m_taskTreeRunner.start(root); +} + IDevice::DeviceInfo AndroidDevice::deviceInformation() const { return IDevice::DeviceInfo(); diff --git a/src/plugins/android/androiddevice.h b/src/plugins/android/androiddevice.h index fbb7e0bccb6..bb93c719ef4 100644 --- a/src/plugins/android/androiddevice.h +++ b/src/plugins/android/androiddevice.h @@ -8,17 +8,11 @@ #include "androiddeviceinfo.h" #include -#include #include -#include - -#include #include -namespace Utils { class Process; } - namespace Android::Internal { class AndroidDevice final : public ProjectExplorer::IDevice @@ -54,6 +48,8 @@ public: QString sdcardSize() const; QString openGLStatus() const; + void startAvd(); + protected: void fromMap(const Utils::Store &map) final; @@ -68,6 +64,7 @@ private: void initAvdSettings(); std::unique_ptr m_avdSettings; + Tasking::TaskTreeRunner m_taskTreeRunner; }; namespace AndroidDeviceManager {