IOS: Fix infinite timeout

Previously the "runCommand" function would never exit if the child process
hangs. This fix makes it so that the caller can specify a function
that determines whether we want to continue to wait.
In this function we then check if the promise has been cancelled.

We also let runCommand return an expected_str to better reflect actual
error reason.

We also early-error in various places to keep indentation low, and
make it easier to track where something returns an error.

Task-number: QTCREATORBUG-29564
Change-Id: I71ee4568d87c6b21c3ba9c71b81d028d517b553a
Reviewed-by: Serg Kryvonos <serg.kryvonos@qt.io>
Reviewed-by: Eike Ziller <eike.ziller@qt.io>
This commit is contained in:
Marcus Tillmanns
2023-10-31 08:44:42 +01:00
parent 892cfccd46
commit d2b7f9d27c
2 changed files with 211 additions and 136 deletions

View File

@@ -31,7 +31,7 @@ static Q_LOGGING_CATEGORY(simulatorLog, "qtc.ios.simulator", QtWarningMsg)
namespace Ios::Internal { namespace Ios::Internal {
const int simulatorStartTimeout = 60000; const std::chrono::seconds simulatorStartTimeout = std::chrono::seconds(60);
// simctl Json Tags and tokens. // simctl Json Tags and tokens.
const char deviceTypeTag[] = "devicetypes"; const char deviceTypeTag[] = "devicetypes";
@@ -47,59 +47,84 @@ const char udidTag[] = "udid";
const char runtimeVersionTag[] = "version"; const char runtimeVersionTag[] = "version";
const char buildVersionTag[] = "buildversion"; const char buildVersionTag[] = "buildversion";
static bool checkForTimeout(const chrono::high_resolution_clock::time_point &start, int msecs = 10000) static expected_str<void> runCommand(
{ const CommandLine &command,
bool timedOut = false; QString *stdOutput,
auto end = chrono::high_resolution_clock::now(); QString *allOutput = nullptr,
if (chrono::duration_cast<chrono::milliseconds>(end-start).count() > msecs) std::function<bool()> shouldStop = [] { return false; })
timedOut = true;
return timedOut;
}
static bool runCommand(const CommandLine &command, QString *stdOutput, QString *allOutput = nullptr)
{ {
Process p; Process p;
p.setTimeoutS(-1);
p.setCommand(command); p.setCommand(command);
p.runBlocking(); p.start();
if (!p.waitForStarted())
return make_unexpected(Tr::tr("Failed to start process."));
forever {
if (shouldStop() || p.waitForFinished(1000))
break;
}
if (p.state() != QProcess::ProcessState::NotRunning) {
p.kill();
if (shouldStop())
return make_unexpected(Tr::tr("Process was canceled."));
return make_unexpected(Tr::tr("Process was forced to exit."));
}
if (stdOutput) if (stdOutput)
*stdOutput = p.cleanedStdOut(); *stdOutput = p.cleanedStdOut();
if (allOutput) if (allOutput)
*allOutput = p.allOutput(); *allOutput = p.allOutput();
return p.result() == ProcessResult::FinishedWithSuccess;
if (p.result() != ProcessResult::FinishedWithSuccess)
return make_unexpected(p.errorString());
return {};
} }
static bool runSimCtlCommand(QStringList args, QString *output, QString *allOutput = nullptr) static expected_str<void> runSimCtlCommand(
QStringList args,
QString *output,
QString *allOutput = nullptr,
std::function<bool()> shouldStop = [] { return false; })
{ {
args.prepend("simctl"); args.prepend("simctl");
// Cache xcrun's path, as this function will be called often. // Cache xcrun's path, as this function will be called often.
static FilePath xcrun = FilePath::fromString("xcrun").searchInPath(); static FilePath xcrun = FilePath::fromString("xcrun").searchInPath();
QTC_ASSERT(!xcrun.isEmpty() && xcrun.isExecutableFile(), xcrun.clear(); return false); if (xcrun.isEmpty())
return runCommand({xcrun, args}, output, allOutput); return make_unexpected(Tr::tr("Cannot find xcrun."));
else if (!xcrun.isExecutableFile())
return make_unexpected(Tr::tr("xcrun is not executable."));
return runCommand({xcrun, args}, output, allOutput, shouldStop);
} }
static bool launchSimulator(const QString &simUdid) { static expected_str<void> launchSimulator(const QString &simUdid, std::function<bool()> shouldStop)
QTC_ASSERT(!simUdid.isEmpty(), return false); {
QTC_ASSERT(!simUdid.isEmpty(), return make_unexpected(Tr::tr("Invalid Empty UDID.")));
const FilePath simulatorAppPath = IosConfigurations::developerPath() const FilePath simulatorAppPath = IosConfigurations::developerPath()
.pathAppended("Applications/Simulator.app/Contents/MacOS/Simulator"); .pathAppended("Applications/Simulator.app/Contents/MacOS/Simulator");
if (IosConfigurations::xcodeVersion() >= QVersionNumber(9)) { if (IosConfigurations::xcodeVersion() >= QVersionNumber(9)) {
// For XCode 9 boot the second device instead of launching simulator app twice. // For XCode 9 boot the second device instead of launching simulator app twice.
QString psOutput; QString psOutput;
if (runCommand({"ps", {"-A", "-o", "comm"}}, &psOutput)) { expected_str<void> result
for (const QString &comm : psOutput.split('\n')) { = runCommand({"ps", {"-A", "-o", "comm"}}, &psOutput, nullptr, shouldStop);
if (comm == simulatorAppPath.toString()) if (!result)
return runSimCtlCommand({"boot", simUdid}, nullptr); return result;
}
} else { for (const QString &comm : psOutput.split('\n')) {
qCDebug(simulatorLog) << "Cannot start Simulator device." if (comm == simulatorAppPath.toString())
<< "Error probing Simulator.app instance"; return runSimCtlCommand({"boot", simUdid}, nullptr, nullptr, shouldStop);
return false;
} }
} }
const bool started = Process::startDetached(
return Process::startDetached({simulatorAppPath, {"--args", "-CurrentDeviceUDID", simUdid}}); {simulatorAppPath, {"--args", "-CurrentDeviceUDID", simUdid}});
if (!started)
return make_unexpected(Tr::tr("Failed to start simulator app."));
return {};
} }
static bool isAvailable(const QJsonObject &object) static bool isAvailable(const QJsonObject &object)
@@ -407,72 +432,78 @@ void startSimulator(QPromise<SimulatorControl::ResponseData> &promise, const QSt
// Shutting down state checks are for the case when simulator start is called within a short // Shutting down state checks are for the case when simulator start is called within a short
// interval of closing the previous interval of the simulator. We wait untill the shutdown // interval of closing the previous interval of the simulator. We wait untill the shutdown
// process is complete. // process is complete.
auto start = chrono::high_resolution_clock::now(); QDeadlineTimer simulatorStartDeadline(simulatorStartTimeout);
while (simInfo.isShuttingDown() && !checkForTimeout(start, simulatorStartTimeout)) { while (simInfo.isShuttingDown() && !simulatorStartDeadline.hasExpired()) {
// Wait till the simulator shuts down, if doing so. // Wait till the simulator shuts down, if doing so.
if (promise.isCanceled()) {
promise.addResult(response.withError(Tr::tr("Simulator start was canceled.")));
return;
}
QThread::msleep(100); QThread::msleep(100);
simInfo = deviceInfo(simUdid); simInfo = deviceInfo(simUdid);
} }
if (simInfo.isShuttingDown()) { if (simInfo.isShuttingDown()) {
promise.addResult(response.withError( promise.addResult(
Tr::tr("Cannot start Simulator device. Previous instance taking " response.withError(Tr::tr("Cannot start Simulator device. Previous instance taking "
"too long to shut down. (name=%1, udid=%2, available=%3, state=%4, runtime=%5)") "too long to shut down. (%1)")
.arg(simInfo.name) .arg(simInfo.toString())));
.arg(simInfo.identifier)
.arg(simInfo.available)
.arg(simInfo.state)
.arg(simInfo.runtimeName)));
return; return;
} }
if (simInfo.isShutdown()) { if (!simInfo.isShutdown()) {
if (launchSimulator(simUdid)) { promise.addResult(response.withError(
if (promise.isCanceled()) Tr::tr("Cannot start Simulator device. Simulator not in shutdown state. (%1)")
return; .arg(simInfo.toString())));
// At this point the sim device exists, available and was not running. return;
// So the simulator is started and we'll wait for it to reach to a state }
// where we can interact with it.
start = chrono::high_resolution_clock::now(); expected_str<void> result = launchSimulator(simUdid,
SimulatorInfo info; [&promise] { return promise.isCanceled(); });
do { if (!result) {
info = deviceInfo(simUdid); promise.addResult(response.withError(result.error()));
if (promise.isCanceled()) return;
return; }
} while (!info.isBooted() && !checkForTimeout(start, simulatorStartTimeout));
if (info.isBooted()) // At this point the sim device exists, available and was not running.
response.success = true; // So the simulator is started and we'll wait for it to reach to a state
} else { // where we can interact with it.
promise.addResult(response.withError(Tr::tr("Error starting simulator."))); // We restart the deadline to give it some more time.
simulatorStartDeadline = QDeadlineTimer(simulatorStartTimeout);
SimulatorInfo info;
do {
info = deviceInfo(simUdid);
if (promise.isCanceled()) {
promise.addResult(response.withError(Tr::tr("Simulator start was canceled.")));
return; return;
} }
} else { } while (!info.isBooted() && !simulatorStartDeadline.hasExpired());
promise.addResult(response.withError(
Tr::tr("Cannot start Simulator device. Simulator not in shutdown state.(name=%1, "
"udid=%2, available=%3, state=%4, runtime=%5)")
.arg(simInfo.name)
.arg(simInfo.identifier)
.arg(simInfo.available)
.arg(simInfo.state)
.arg(simInfo.runtimeName)));
return;
}
if (!promise.isCanceled()) if (info.isBooted())
promise.addResult(response); response.success = true;
promise.addResult(response.withSuccess());
} }
void installApp(QPromise<SimulatorControl::ResponseData> &promise, void installApp(QPromise<SimulatorControl::ResponseData> &promise,
const QString &simUdid, const Utils::FilePath &bundlePath) const QString &simUdid, const Utils::FilePath &bundlePath)
{ {
QTC_CHECK(bundlePath.exists());
SimulatorControl::ResponseData response(simUdid); SimulatorControl::ResponseData response(simUdid);
response.success = runSimCtlCommand({"install", simUdid, bundlePath.toString()},
nullptr, if (!bundlePath.exists()) {
&response.commandOutput); promise.addResult(response.withError(Tr::tr("Bundle path does not exist.")));
if (!promise.isCanceled()) return;
promise.addResult(response); }
expected_str<void> result = runSimCtlCommand({"install", simUdid, bundlePath.toString()},
nullptr,
&response.commandOutput,
[&promise] { return promise.isCanceled(); });
if (!result)
promise.addResult(response.withError(result.error()));
else
promise.addResult(response.withSuccess());
} }
void launchApp(QPromise<SimulatorControl::ResponseData> &promise, void launchApp(QPromise<SimulatorControl::ResponseData> &promise,
@@ -484,53 +515,74 @@ void launchApp(QPromise<SimulatorControl::ResponseData> &promise,
const QString &stderrPath) const QString &stderrPath)
{ {
SimulatorControl::ResponseData response(simUdid); SimulatorControl::ResponseData response(simUdid);
if (!bundleIdentifier.isEmpty() && !promise.isCanceled()) {
QStringList args({"launch", simUdid, bundleIdentifier});
// simctl usage documentation : Note: Log output is often directed to stderr, not stdout. if (bundleIdentifier.isEmpty()) {
if (!stdoutPath.isEmpty()) promise.addResult(response.withError(Tr::tr("Invalid (empty) bundle identifier.")));
args.insert(1, QString("--stderr=%1").arg(stdoutPath)); return;
if (!stderrPath.isEmpty())
args.insert(1, QString("--stdout=%1").arg(stderrPath));
if (waitForDebugger)
args.insert(1, "-w");
for (const QString &extraArgument : extraArgs) {
if (!extraArgument.trimmed().isEmpty())
args << extraArgument;
}
QString stdOutput;
if (runSimCtlCommand(args, &stdOutput, &response.commandOutput)) {
const QString pIdStr = stdOutput.trimmed().split(' ').last().trimmed();
bool validPid = false;
response.pID = pIdStr.toLongLong(&validPid);
response.success = validPid;
}
} }
if (!promise.isCanceled()) QStringList args({"launch", simUdid, bundleIdentifier});
promise.addResult(response);
// simctl usage documentation : Note: Log output is often directed to stderr, not stdout.
if (!stdoutPath.isEmpty())
args.insert(1, QString("--stderr=%1").arg(stdoutPath));
if (!stderrPath.isEmpty())
args.insert(1, QString("--stdout=%1").arg(stderrPath));
if (waitForDebugger)
args.insert(1, "-w");
for (const QString &extraArgument : extraArgs) {
if (!extraArgument.trimmed().isEmpty())
args << extraArgument;
}
QString stdOutput;
expected_str<void> result = runSimCtlCommand(args,
&stdOutput,
&response.commandOutput,
[&promise] { return promise.isCanceled(); });
if (!result) {
promise.addResult(response.withError(result.error()));
return;
}
const QString pIdStr = stdOutput.trimmed().split(' ').last().trimmed();
bool validPid = false;
response.pID = pIdStr.toLongLong(&validPid);
response.success = validPid;
promise.addResult(response.withSuccess());
} }
void deleteSimulator(QPromise<SimulatorControl::ResponseData> &promise, const QString &simUdid) void deleteSimulator(QPromise<SimulatorControl::ResponseData> &promise, const QString &simUdid)
{ {
SimulatorControl::ResponseData response(simUdid); SimulatorControl::ResponseData response(simUdid);
response.success = runSimCtlCommand({"delete", simUdid}, nullptr, &response.commandOutput); expected_str<void> result = runSimCtlCommand({"delete", simUdid},
nullptr,
&response.commandOutput,
[&promise] { return promise.isCanceled(); });
if (!promise.isCanceled()) if (!result)
promise.addResult(response); promise.addResult(response.withError(result.error()));
else
promise.addResult(response.withSuccess());
} }
void resetSimulator(QPromise<SimulatorControl::ResponseData> &promise, const QString &simUdid) void resetSimulator(QPromise<SimulatorControl::ResponseData> &promise, const QString &simUdid)
{ {
SimulatorControl::ResponseData response(simUdid); SimulatorControl::ResponseData response(simUdid);
response.success = runSimCtlCommand({"erase", simUdid}, nullptr, &response.commandOutput); expected_str<void> result = runSimCtlCommand({"erase", simUdid},
nullptr,
&response.commandOutput,
[&promise] { return promise.isCanceled(); });
if (!promise.isCanceled()) if (!result)
promise.addResult(response); promise.addResult(response.withError(result.error()));
else
promise.addResult(response.withSuccess());
} }
void renameSimulator(QPromise<SimulatorControl::ResponseData> &promise, void renameSimulator(QPromise<SimulatorControl::ResponseData> &promise,
@@ -538,11 +590,14 @@ void renameSimulator(QPromise<SimulatorControl::ResponseData> &promise,
const QString &newName) const QString &newName)
{ {
SimulatorControl::ResponseData response(simUdid); SimulatorControl::ResponseData response(simUdid);
response.success = runSimCtlCommand({"rename", simUdid, newName}, expected_str<void> result = runSimCtlCommand({"rename", simUdid, newName},
nullptr, nullptr,
&response.commandOutput); &response.commandOutput,
if (!promise.isCanceled()) [&promise] { return promise.isCanceled(); });
promise.addResult(response); if (!result)
promise.addResult(response.withError(result.error()));
else
promise.addResult(response.withSuccess());
} }
void createSimulator(QPromise<SimulatorControl::ResponseData> &promise, void createSimulator(QPromise<SimulatorControl::ResponseData> &promise,
@@ -551,17 +606,24 @@ void createSimulator(QPromise<SimulatorControl::ResponseData> &promise,
const RuntimeInfo &runtime) const RuntimeInfo &runtime)
{ {
SimulatorControl::ResponseData response("Invalid"); SimulatorControl::ResponseData response("Invalid");
if (!name.isEmpty()) {
QString stdOutput; if (name.isEmpty()) {
response.success promise.addResult(response);
= runSimCtlCommand({"create", name, deviceType.identifier, runtime.identifier}, return;
&stdOutput,
&response.commandOutput);
response.simUdid = response.success ? stdOutput.trimmed() : QString();
} }
if (!promise.isCanceled()) QString stdOutput;
promise.addResult(response); expected_str<void> result
= runSimCtlCommand({"create", name, deviceType.identifier, runtime.identifier},
&stdOutput,
&response.commandOutput,
[&promise] { return promise.isCanceled(); });
response.simUdid = result ? stdOutput.trimmed() : QString();
if (!result)
promise.addResult(response.withError(result.error()));
else
promise.addResult(response.withSuccess());
} }
void takeSceenshot(QPromise<SimulatorControl::ResponseData> &promise, void takeSceenshot(QPromise<SimulatorControl::ResponseData> &promise,
@@ -569,19 +631,25 @@ void takeSceenshot(QPromise<SimulatorControl::ResponseData> &promise,
const QString &filePath) const QString &filePath)
{ {
SimulatorControl::ResponseData response(simUdid); SimulatorControl::ResponseData response(simUdid);
response.success = runSimCtlCommand({"io", simUdid, "screenshot", filePath}, expected_str<void> result = runSimCtlCommand({"io", simUdid, "screenshot", filePath},
nullptr, nullptr,
&response.commandOutput); &response.commandOutput,
if (!promise.isCanceled()) [&promise] { return promise.isCanceled(); });
promise.addResult(response);
if (!result)
promise.addResult(response.withError(result.error()));
else
promise.addResult(response.withSuccess());
} }
QDebug &operator<<(QDebug &stream, const SimulatorInfo &info) QString SimulatorInfo::toString() const
{ {
stream << "Name: " << info.name << "UDID: " << info.identifier return QString("Name: %1 UDID: %2 Availability: %3 State: %4 Runtime: %5")
<< "Availability: " << info.available << "State: " << info.state .arg(name)
<< "Runtime: " << info.runtimeName; .arg(identifier)
return stream; .arg(available)
.arg(state)
.arg(runtimeName);
} }
bool SimulatorInfo::operator==(const SimulatorInfo &other) const bool SimulatorInfo::operator==(const SimulatorInfo &other) const

View File

@@ -28,9 +28,9 @@ public:
class SimulatorInfo : public SimulatorEntity class SimulatorInfo : public SimulatorEntity
{ {
friend QDebug &operator<<(QDebug &, const SimulatorInfo &info);
public: public:
QString toString() const;
bool isBooted() const { return state == "Booted"; } bool isBooted() const { return state == "Booted"; }
bool isShuttingDown() const { return state == "Shutting Down"; } bool isShuttingDown() const { return state == "Shutting Down"; }
bool isShutdown() const { return state == "Shutdown"; } bool isShutdown() const { return state == "Shutdown"; }
@@ -69,6 +69,13 @@ public:
result.success = false; result.success = false;
return result; return result;
} }
ResponseData withSuccess()
{
ResponseData result = std::move(*this);
result.success = true;
return result;
}
}; };
public: public: