Android: Make AndroidDeployQtStep::runRecipe() thread safe

Reuse existing avd recipes and run a task tree instead.

Change-Id: I515e15aac8e047b6da72d7604a033824e1b99107
Reviewed-by: Alessandro Portale <alessandro.portale@qt.io>
This commit is contained in:
Jarek Kobus
2024-07-05 19:19:02 +02:00
parent 33e548d52b
commit 59c10c1274

View File

@@ -32,19 +32,16 @@
#include <qtsupport/baseqtversion.h> #include <qtsupport/baseqtversion.h>
#include <qtsupport/qtkitaspect.h> #include <qtsupport/qtkitaspect.h>
#include <solutions/tasking/conditional.h>
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/async.h> #include <utils/async.h>
#include <utils/commandline.h> #include <utils/commandline.h>
#include <utils/environment.h> #include <utils/environment.h>
#include <utils/futuresynchronizer.h>
#include <utils/layoutbuilder.h> #include <utils/layoutbuilder.h>
#include <utils/qtcprocess.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/qtcprocess.h>
#include <QCheckBox>
#include <QFileDialog>
#include <QGroupBox>
#include <QInputDialog>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
@@ -73,8 +70,7 @@ enum DeployErrorFlag
InconsistentCertificates = 0x0001, InconsistentCertificates = 0x0001,
UpdateIncompatible = 0x0002, UpdateIncompatible = 0x0002,
PermissionModelDowngrade = 0x0004, PermissionModelDowngrade = 0x0004,
VersionDowngrade = 0x0008, VersionDowngrade = 0x0008
Failure = 0x0010
}; };
Q_DECLARE_FLAGS(DeployErrorFlags, DeployErrorFlag) Q_DECLARE_FLAGS(DeployErrorFlags, DeployErrorFlag)
@@ -137,18 +133,10 @@ class AndroidDeployQtStep : public BuildStep
public: public:
AndroidDeployQtStep(BuildStepList *bc, Id id); AndroidDeployQtStep(BuildStepList *bc, Id id);
signals:
void askForUninstall(DeployErrorFlags errorCode);
private: private:
void runCommand(const CommandLine &command);
bool init() override; bool init() override;
GroupItem runRecipe() final; GroupItem runRecipe() final;
DeployErrorFlags runDeploy(QPromise<void> &promise); Group deployRecipe();
void slotAskForUninstall(DeployErrorFlags errorFlags);
void runImpl(QPromise<void> &promise);
QWidget *createConfigWidget() override; QWidget *createConfigWidget() override;
@@ -162,19 +150,16 @@ private:
QString m_serialNumber; QString m_serialNumber;
QString m_avdName; QString m_avdName;
FilePath m_apkPath; FilePath m_apkPath;
QList<FileToPull> m_filesToPull;
BoolAspect m_uninstallPreviousPackage{this}; BoolAspect m_uninstallPreviousPackage{this};
bool m_uninstallPreviousPackageRun = false; bool m_uninstallPreviousPackageRun = false;
bool m_useAndroiddeployqt = false; bool m_useAndroiddeployqt = false;
bool m_askForUninstall = false;
CommandLine m_androiddeployqtArgs; CommandLine m_androiddeployqtArgs;
FilePath m_adbPath; FilePath m_adbPath;
FilePath m_command; FilePath m_command;
FilePath m_workingDirectory; FilePath m_workingDirectory;
Environment m_environment; Environment m_environment;
AndroidDeviceInfo m_deviceInfo; AndroidDeviceInfo m_deviceInfo;
FutureSynchronizer m_synchronizer;
}; };
AndroidDeployQtStep::AndroidDeployQtStep(BuildStepList *parent, Id id) AndroidDeployQtStep::AndroidDeployQtStep(BuildStepList *parent, Id id)
@@ -194,10 +179,6 @@ AndroidDeployQtStep::AndroidDeployQtStep(BuildStepList *parent, Id id)
m_uninstallPreviousPackage.setValue(true); m_uninstallPreviousPackage.setValue(true);
m_uninstallPreviousPackage.setEnabled(false); m_uninstallPreviousPackage.setEnabled(false);
} }
connect(this, &AndroidDeployQtStep::askForUninstall,
this, &AndroidDeployQtStep::slotAskForUninstall,
Qt::BlockingQueuedConnection);
} }
bool AndroidDeployQtStep::init() bool AndroidDeployQtStep::init()
@@ -309,9 +290,6 @@ bool AndroidDeployQtStep::init()
AndroidManager::setDeviceApiLevel(target(), info.sdk); AndroidManager::setDeviceApiLevel(target(), info.sdk);
AndroidManager::setDeviceAbis(target(), info.cpuAbi); AndroidManager::setDeviceAbis(target(), info.cpuAbi);
if (m_deviceInfo.isValid())
m_filesToPull = filesToPull(target());
emit addOutput(Tr::tr("Deploying to %1").arg(m_serialNumber), OutputFormat::NormalMessage); emit addOutput(Tr::tr("Deploying to %1").arg(m_serialNumber), OutputFormat::NormalMessage);
m_uninstallPreviousPackageRun = m_uninstallPreviousPackage(); m_uninstallPreviousPackageRun = m_uninstallPreviousPackage();
@@ -371,209 +349,222 @@ bool AndroidDeployQtStep::init()
m_environment = bc ? bc->environment() : Environment(); m_environment = bc ? bc->environment() : Environment();
m_adbPath = AndroidConfig::adbToolPath(); m_adbPath = AndroidConfig::adbToolPath();
// Start the AVD if not running.
if (!m_avdName.isEmpty() && AndroidAvdManager::findAvd(m_avdName).isEmpty())
AndroidAvdManager::startAvdAsync(m_avdName);
return true; return true;
} }
DeployErrorFlags AndroidDeployQtStep::runDeploy(QPromise<void> &promise) static void removeFile(const FilePath &path) { path.removeFile(); }
GroupItem AndroidDeployQtStep::runRecipe()
{ {
CommandLine cmd(m_command); const Storage<QString> serialNumberStorage;
if (m_useAndroiddeployqt && m_apkPath.isEmpty()) {
cmd.addArgs(m_androiddeployqtArgs.arguments(), CommandLine::Raw);
if (m_uninstallPreviousPackageRun)
cmd.addArg("--install");
else
cmd.addArg("--reinstall");
if (!m_serialNumber.isEmpty() && !m_serialNumber.startsWith("????")) const auto onSerialNumberGroupSetup = [this] {
cmd.addArgs({"--device", m_serialNumber}); return m_avdName.isEmpty() ? SetupResult::StopWithSuccess : SetupResult::Continue;
};
} else { const auto onSerialNumberGroupDone = [this, serialNumberStorage] {
RunConfiguration *rc = target()->activeRunConfiguration(); const QString serialNumber = *serialNumberStorage;
QTC_ASSERT(rc, return Failure);
QString packageName;
if (m_uninstallPreviousPackageRun) {
packageName = AndroidManager::packageName(target());
if (packageName.isEmpty()) {
reportWarningOrError(
Tr::tr("Cannot find the package name from AndroidManifest.xml nor "
"build.gradle files at \"%1\".")
.arg(AndroidManager::androidBuildDirectory(target()).toUserOutput()),
Task::Error);
return Failure;
}
const QString msg = Tr::tr("Uninstalling the previous package \"%1\".").arg(packageName);
qCDebug(deployStepLog) << msg;
emit addOutput(msg, OutputFormat::NormalMessage);
runCommand({m_adbPath,
{AndroidDeviceInfo::adbSelector(m_serialNumber), "uninstall", packageName}});
}
cmd.addArgs(AndroidDeviceInfo::adbSelector(m_serialNumber));
cmd.addArgs({"install", "-r", m_apkPath.toString()});
}
Process process;
process.setCommand(cmd);
process.setWorkingDirectory(m_workingDirectory);
process.setEnvironment(m_environment);
process.setUseCtrlCStub(true);
DeployErrorFlags deployError = NoError;
process.setStdOutLineCallback([this, &deployError](const QString &line) {
deployError |= parseDeployErrors(line);
stdOutput(line);
});
process.setStdErrLineCallback([this, &deployError](const QString &line) {
deployError |= parseDeployErrors(line);
stdError(line);
});
process.start();
emit addOutput(Tr::tr("Starting: \"%1\"").arg(cmd.toUserOutput()), OutputFormat::NormalMessage);
while (!process.waitForFinished(200ms)) {
if (process.state() == QProcess::NotRunning)
break;
if (promise.isCanceled()) {
process.kill();
process.waitForFinished();
}
}
const QProcess::ExitStatus exitStatus = process.exitStatus();
const int exitCode = process.exitCode();
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
emit addOutput(Tr::tr("The process \"%1\" exited normally.").arg(m_command.toUserOutput()),
OutputFormat::NormalMessage);
} else if (exitStatus == QProcess::NormalExit) {
const QString error = Tr::tr("The process \"%1\" exited with code %2.")
.arg(m_command.toUserOutput(), QString::number(exitCode));
reportWarningOrError(error, Task::Error);
} else {
const QString error = Tr::tr("The process \"%1\" crashed.").arg(m_command.toUserOutput());
reportWarningOrError(error, Task::Error);
}
if (deployError != NoError) {
if (m_uninstallPreviousPackageRun) {
deployError = Failure; // Even re-install failed. Set to Failure.
reportWarningOrError(
Tr::tr("Installing the app failed even after uninstalling the previous one."),
Task::Error);
}
} 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.
reportWarningOrError(Tr::tr("Installing the app failed with an unknown error."), Task::Error);
deployError = Failure;
}
return deployError;
}
void AndroidDeployQtStep::slotAskForUninstall(DeployErrorFlags errorFlags)
{
Q_ASSERT(errorFlags > 0);
QString uninstallMsg = Tr::tr("Deployment failed with the following errors:") + "\n\n";
if (errorFlags & InconsistentCertificates)
uninstallMsg += InstallFailedInconsistentCertificatesString + '\n';
if (errorFlags & UpdateIncompatible)
uninstallMsg += InstallFailedUpdateIncompatible + '\n';
if (errorFlags & PermissionModelDowngrade)
uninstallMsg += InstallFailedPermissionModelDowngrade + '\n';
if (errorFlags & VersionDowngrade)
uninstallMsg += InstallFailedVersionDowngrade + '\n';
uninstallMsg += '\n';
uninstallMsg.append(Tr::tr("Uninstalling the installed package may solve the issue.") + '\n');
uninstallMsg.append(Tr::tr("Do you want to uninstall the existing package?"));
m_askForUninstall = QMessageBox::critical(nullptr, Tr::tr("Install failed"), uninstallMsg,
QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes;
}
// TODO: This implementation is not thread safe.
void AndroidDeployQtStep::runImpl(QPromise<void> &promise)
{
if (!m_avdName.isEmpty()) {
const QString serialNumber = AndroidAvdManager::waitForAvd(m_avdName, promise.future());
qCDebug(deployStepLog) << "Deploying to AVD:" << m_avdName << serialNumber;
if (serialNumber.isEmpty()) { if (serialNumber.isEmpty()) {
reportWarningOrError(Tr::tr("The deployment AVD \"%1\" cannot be started.") reportWarningOrError(Tr::tr("The deployment AVD \"%1\" cannot be started.")
.arg(m_avdName), Task::Error); .arg(m_avdName), Task::Error);
promise.future().cancel(); return false;
return;
} }
m_serialNumber = serialNumber; m_serialNumber = serialNumber;
qCDebug(deployStepLog) << "Deployment device serial number changed:" << serialNumber; qCDebug(deployStepLog) << "Deployment device serial number changed:" << serialNumber;
AndroidManager::setDeviceSerialNumber(target(), serialNumber); AndroidManager::setDeviceSerialNumber(target(), serialNumber);
} return true;
};
DeployErrorFlags returnValue = runDeploy(promise); const LoopList iterator(m_deviceInfo.isValid() ? filesToPull(target()) : QList<FileToPull>());
if (returnValue > NoError && returnValue < Failure) { const auto onRemoveFileSetup = [iterator](Async<void> &async) {
emit askForUninstall(returnValue); async.setConcurrentCallData(removeFile, iterator->to);
if (m_askForUninstall) { };
m_uninstallPreviousPackageRun = true;
returnValue = runDeploy(promise);
}
}
if (!m_filesToPull.isEmpty()) const auto onAdbSetup = [this, iterator](Process &process) {
emit addOutput(Tr::tr("Pulling files necessary for debugging."), OutputFormat::NormalMessage); const FileToPull &file = *iterator;
// Note that values are not necessarily unique, e.g. app_process is looked up in several
// directories
for (const FileToPull &file : std::as_const(m_filesToPull))
file.to.removeFile();
for (const FileToPull &file : std::as_const(m_filesToPull)) {
const FilePath parentDir = file.to.parentDir(); const FilePath parentDir = file.to.parentDir();
if (!parentDir.ensureWritableDir()) { if (!parentDir.ensureWritableDir()) {
const QString error = QString("Package deploy: Unable to create directory %1.") const QString error = QString("Package deploy: Unable to create directory %1.")
.arg(parentDir.nativePath()); .arg(parentDir.nativePath());
reportWarningOrError(error, Task::Error); reportWarningOrError(error, Task::Error);
} }
const CommandLine cmd{m_adbPath, {AndroidDeviceInfo::adbSelector(m_serialNumber),
runCommand({m_adbPath, {AndroidDeviceInfo::adbSelector(m_serialNumber), "pull", file.from, "pull", file.from, file.to.nativePath()}};
file.to.nativePath()}}); emit addOutput(Tr::tr("Package deploy: Running command \"%1\".").arg(cmd.toUserOutput()),
OutputFormat::NormalMessage);
process.setCommand(cmd);
};
const auto onAdbDone = [this, iterator](const Process &process, DoneWith result) {
if (result != DoneWith::Success) {
reportWarningOrError(process.exitMessage(), Task::Error);
}
const FileToPull &file = *iterator;
if (!file.to.exists()) { if (!file.to.exists()) {
const QString error = Tr::tr("Package deploy: Failed to pull \"%1\" to \"%2\".") const QString error = Tr::tr("Package deploy: Failed to pull \"%1\" to \"%2\".")
.arg(file.from, file.to.nativePath()); .arg(file.from, file.to.nativePath());
reportWarningOrError(error, Task::Error); reportWarningOrError(error, Task::Error);
} }
} return true;
if (returnValue != NoError) };
promise.future().cancel();
} return Group {
Group {
GroupItem AndroidDeployQtStep::runRecipe() serialNumberStorage,
{ onGroupSetup(onSerialNumberGroupSetup),
const auto onSetup = [this](Async<void> &async) { AndroidAvdManager::startAvdRecipe(m_avdName, serialNumberStorage),
async.setConcurrentCallData(&AndroidDeployQtStep::runImpl, this); onGroupDone(onSerialNumberGroupDone)
async.setFutureSynchronizer(&m_synchronizer); },
deployRecipe(),
For {
iterator,
parallelIdealThreadCountLimit,
AsyncTask<void>(onRemoveFileSetup)
},
For {
iterator,
ProcessTask(onAdbSetup, onAdbDone)
}
}; };
return AsyncTask<void>(onSetup);
} }
void AndroidDeployQtStep::runCommand(const CommandLine &command) Group AndroidDeployQtStep::deployRecipe()
{ {
Process buildProc; const Storage<DeployErrorFlags> storage;
emit addOutput(Tr::tr("Package deploy: Running command \"%1\".").arg(command.toUserOutput()),
OutputFormat::NormalMessage);
buildProc.setCommand(command); const auto onUninstallSetup = [this](Process &process) {
buildProc.runBlocking(2min, EventLoopMode::On); if (m_useAndroiddeployqt && m_apkPath.isEmpty())
if (buildProc.result() != ProcessResult::FinishedWithSuccess) return SetupResult::StopWithSuccess;
reportWarningOrError(buildProc.exitMessage(), Task::Error); if (!m_uninstallPreviousPackageRun)
return SetupResult::StopWithSuccess;
QTC_ASSERT(target()->activeRunConfiguration(), return SetupResult::StopWithError);
const QString packageName = AndroidManager::packageName(target());
if (packageName.isEmpty()) {
reportWarningOrError(
Tr::tr("Cannot find the package name from AndroidManifest.xml nor "
"build.gradle files at \"%1\".")
.arg(AndroidManager::androidBuildDirectory(target()).toUserOutput()),
Task::Error);
return SetupResult::StopWithError;
}
const QString msg = Tr::tr("Uninstalling the previous package \"%1\".").arg(packageName);
qCDebug(deployStepLog) << msg;
emit addOutput(msg, OutputFormat::NormalMessage);
const CommandLine cmd{m_adbPath, {AndroidDeviceInfo::adbSelector(m_serialNumber),
"uninstall", packageName}};
emit addOutput(Tr::tr("Package deploy: Running command \"%1\".").arg(cmd.toUserOutput()),
OutputFormat::NormalMessage);
process.setCommand(cmd);
return SetupResult::Continue;
};
const auto onUninstallDone = [this](const Process &process) {
reportWarningOrError(process.exitMessage(), Task::Error);
};
const auto onInstallSetup = [this, storage](Process &process) {
CommandLine cmd(m_command);
if (m_useAndroiddeployqt && m_apkPath.isEmpty()) {
cmd.addArgs(m_androiddeployqtArgs.arguments(), CommandLine::Raw);
if (m_uninstallPreviousPackageRun)
cmd.addArg("--install");
else
cmd.addArg("--reinstall");
if (!m_serialNumber.isEmpty() && !m_serialNumber.startsWith("????"))
cmd.addArgs({"--device", m_serialNumber});
} else {
QTC_ASSERT(target()->activeRunConfiguration(), return SetupResult::StopWithError);
cmd.addArgs(AndroidDeviceInfo::adbSelector(m_serialNumber));
cmd.addArgs({"install", "-r", m_apkPath.toString()});
}
process.setCommand(cmd);
process.setWorkingDirectory(m_workingDirectory);
process.setEnvironment(m_environment);
process.setUseCtrlCStub(true);
DeployErrorFlags *flagsPtr = storage.activeStorage();
process.setStdOutLineCallback([this, flagsPtr](const QString &line) {
*flagsPtr |= parseDeployErrors(line);
stdOutput(line);
});
process.setStdErrLineCallback([this, flagsPtr](const QString &line) {
*flagsPtr |= parseDeployErrors(line);
stdError(line);
});
emit addOutput(Tr::tr("Starting: \"%1\"").arg(cmd.toUserOutput()), OutputFormat::NormalMessage);
return SetupResult::Continue;
};
const auto onInstallDone = [this, storage](const Process &process) {
const QProcess::ExitStatus exitStatus = process.exitStatus();
const int exitCode = process.exitCode();
if (exitStatus == QProcess::NormalExit && exitCode == 0) {
emit addOutput(Tr::tr("The process \"%1\" exited normally.").arg(m_command.toUserOutput()),
OutputFormat::NormalMessage);
} else if (exitStatus == QProcess::NormalExit) {
const QString error = Tr::tr("The process \"%1\" exited with code %2.")
.arg(m_command.toUserOutput(), QString::number(exitCode));
reportWarningOrError(error, Task::Error);
} else {
const QString error = Tr::tr("The process \"%1\" crashed.").arg(m_command.toUserOutput());
reportWarningOrError(error, Task::Error);
}
if (*storage != NoError) {
if (m_uninstallPreviousPackageRun) {
reportWarningOrError(
Tr::tr("Installing the app failed even after uninstalling the previous one."),
Task::Error);
*storage = NoError;
return false;
}
} 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.
reportWarningOrError(Tr::tr("Installing the app failed with an unknown error."), Task::Error);
return false;
}
return true;
};
const auto onAskForUninstallSetup = [this, storage] {
const DeployErrorFlags &errorFlags = *storage;
QString uninstallMsg = Tr::tr("Deployment failed with the following errors:") + "\n\n";
if (errorFlags & InconsistentCertificates)
uninstallMsg += InstallFailedInconsistentCertificatesString + '\n';
if (errorFlags & UpdateIncompatible)
uninstallMsg += InstallFailedUpdateIncompatible + '\n';
if (errorFlags & PermissionModelDowngrade)
uninstallMsg += InstallFailedPermissionModelDowngrade + '\n';
if (errorFlags & VersionDowngrade)
uninstallMsg += InstallFailedVersionDowngrade + '\n';
uninstallMsg += '\n';
uninstallMsg.append(Tr::tr("Uninstalling the installed package may solve the issue.") + '\n');
uninstallMsg.append(Tr::tr("Do you want to uninstall the existing package?"));
if (QMessageBox::critical(nullptr, Tr::tr("Install failed"), uninstallMsg,
QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) {
m_uninstallPreviousPackageRun = true;
return SetupResult::Continue;
}
return SetupResult::StopWithSuccess;
};
return Group {
storage,
Group {
ProcessTask(onUninstallSetup, onUninstallDone, CallDoneIf::Error).withTimeout(2min),
ProcessTask(onInstallSetup, onInstallDone),
onGroupDone(DoneResult::Success)
},
If ([storage] { return *storage != NoError; }) >> Then {
onGroupSetup(onAskForUninstallSetup),
ProcessTask(onUninstallSetup, onUninstallDone, CallDoneIf::Error).withTimeout(2min),
ProcessTask(onInstallSetup, onInstallDone),
onGroupDone(DoneResult::Success)
}
};
} }
QWidget *AndroidDeployQtStep::createConfigWidget() QWidget *AndroidDeployQtStep::createConfigWidget()
@@ -628,7 +619,6 @@ void AndroidDeployQtStep::stdError(const QString &line)
} }
} }
void AndroidDeployQtStep::reportWarningOrError(const QString &message, Task::TaskType type) void AndroidDeployQtStep::reportWarningOrError(const QString &message, Task::TaskType type)
{ {
qCDebug(deployStepLog).noquote() << message; qCDebug(deployStepLog).noquote() << message;