// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "iostool.h" #include "relayserver.h" #include "gdbrunner.h" #include #include #include #include #include namespace Ios { IosTool::IosTool(QObject *parent): QObject(parent), m_xmlWriter(&m_outputFile) { m_outputFile.open(stdout, QIODevice::WriteOnly, QFileDevice::DontCloseHandle); m_xmlWriter.setAutoFormatting(true); } IosTool::~IosTool() { } void IosTool::run(const QStringList &args) { IosDeviceManager *manager = IosDeviceManager::instance(); QString deviceId; QString bundlePath; bool deviceInfo = false; bool printHelp = false; int timeout = 1000; QStringList extraArgs; m_xmlWriter.writeStartDocument(); m_xmlWriter.writeStartElement(QLatin1String("query_result")); QCommandLineParser cmdLineParser; cmdLineParser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); cmdLineParser.setApplicationDescription("iOS Deployment and run helper"); const QList options{ QCommandLineOption(QStringList{"h", "help"}), QCommandLineOption(QStringList{"i", "id"}, "target device id", "id"), QCommandLineOption(QStringList{"b", "bundle"}, "path to the target bundle.app", "bundle"), QCommandLineOption("delta-path", "path to the deploy delta directory", "delta-path"), QCommandLineOption("install", "Install a bundle on a device"), QCommandLineOption(QStringList{"r", "run"}, "Run the bundle on the device"), QCommandLineOption(QStringList{"d", "debug"}, "Debug the bundle on the device"), QCommandLineOption("device-info", "Retrieve information about the selected device"), QCommandLineOption(QStringList{"t", "timeout"}, "Operation timeout (s)", "timeout"), QCommandLineOption(QStringList{"v", "verbose"}, "Verbose output"), }; cmdLineParser.addOptions(options); cmdLineParser.process(args); if (cmdLineParser.isSet("verbose")) m_echoRelays = true; if (cmdLineParser.isSet("id")) deviceId = cmdLineParser.value("id"); if (cmdLineParser.isSet("bundle")) bundlePath = cmdLineParser.value("bundle"); if (cmdLineParser.isSet("delta-path")) m_deltasPath = cmdLineParser.value("delta-path"); if (cmdLineParser.isSet("install")) { m_requestedOperation = IosDeviceManager::AppOp(m_requestedOperation | IosDeviceManager::Install); } else if (cmdLineParser.isSet("run")) { m_requestedOperation = IosDeviceManager::AppOp(m_requestedOperation | IosDeviceManager::Run); } if (cmdLineParser.isSet("debug")) { m_requestedOperation = IosDeviceManager::AppOp(m_requestedOperation | IosDeviceManager::Run); m_debug = true; } if (cmdLineParser.isSet("device-info")) deviceInfo = true; if (cmdLineParser.isSet("timeout")) { bool ok = false; timeout = cmdLineParser.value("timeout").toInt(&ok); if (!ok || timeout < 0) { writeError("timeout value should be an integer"); printHelp = true; } } extraArgs = cmdLineParser.positionalArguments(); if (printHelp || cmdLineParser.isSet("help")) { m_xmlWriter.writeStartElement(QLatin1String("msg")); m_xmlWriter.writeCharacters( QLatin1String("iostool [--id ] [--bundle ] [--delta-path] " "[--install] [--run] [--debug]\n")); m_xmlWriter.writeCharacters(QLatin1String( " [--device-info] [--timeout ] [--verbose]\n")); // to do pass in env as stub does m_xmlWriter.writeCharacters(QLatin1String(" [-- ]")); m_xmlWriter.writeEndElement(); doExit(-1); return; } m_outputFile.flush(); connect(manager, &IosDeviceManager::isTransferringApp, this, &IosTool::isTransferringApp); connect(manager, &IosDeviceManager::didTransferApp, this, &IosTool::didTransferApp); connect(manager, &IosDeviceManager::didStartApp, this, &IosTool::didStartApp); connect(manager, &IosDeviceManager::deviceInfo, this, &IosTool::deviceInfo); connect(manager, &IosDeviceManager::appOutput, this, &IosTool::appOutput); connect(manager, &IosDeviceManager::message, this, &IosTool::writeMsg); connect(manager, &IosDeviceManager::errorMsg, this, &IosTool::writeError); manager->watchDevices(); const QRegularExpression qmlPortRe(QLatin1String("-qmljsdebugger=port:([0-9]+)")); for (const QString &arg : extraArgs) { const QRegularExpressionMatch match = qmlPortRe.match(arg); if (match.hasMatch()) { bool ok; int qmlPort = match.captured(1).toInt(&ok); if (ok && qmlPort > 0 && qmlPort <= 0xFFFF) m_qmlPort = match.captured(1); break; } } if (deviceInfo) { if (!bundlePath.isEmpty()) writeMsg("--device-info overrides --bundle"); ++m_operationsRemaining; manager->requestDeviceInfo(deviceId, timeout); } else if (!bundlePath.isEmpty()) { switch (m_requestedOperation) { case IosDeviceManager::None: break; case IosDeviceManager::Install: case IosDeviceManager::Run: ++m_operationsRemaining; break; case IosDeviceManager::InstallAndRun: m_operationsRemaining += 2; break; } m_maxProgress = 200; manager->requestAppOp(bundlePath, extraArgs, m_requestedOperation, deviceId, timeout, m_deltasPath); } if (m_operationsRemaining == 0) doExit(0); } void IosTool::stopXml(int errorCode) { QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeEmptyElement(QLatin1String("exit")); m_xmlWriter.writeAttribute(QLatin1String("code"), QString::number(errorCode)); m_xmlWriter.writeEndElement(); // result element (hopefully) m_xmlWriter.writeEndDocument(); m_outputFile.flush(); } void IosTool::doExit(int errorCode) { stopXml(errorCode); QCoreApplication::exit(errorCode); // sometime does not really exit exit(errorCode); } void IosTool::isTransferringApp(const QString &bundlePath, const QString &deviceId, int progress, const QString &info) { Q_UNUSED(bundlePath) Q_UNUSED(deviceId) QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeStartElement(QLatin1String("status")); m_xmlWriter.writeAttribute(QLatin1String("progress"), QString::number(progress)); m_xmlWriter.writeAttribute(QLatin1String("max_progress"), QString::number(m_maxProgress)); m_xmlWriter.writeCharacters(info); m_xmlWriter.writeEndElement(); m_outputFile.flush(); } void IosTool::didTransferApp(const QString &bundlePath, const QString &deviceId, IosDeviceManager::OpStatus status) { Q_UNUSED(bundlePath) Q_UNUSED(deviceId) { QMutexLocker l(&m_xmlMutex); if (status == IosDeviceManager::Success) { m_xmlWriter.writeStartElement(QLatin1String("status")); m_xmlWriter.writeAttribute(QLatin1String("progress"), QString::number(m_maxProgress)); m_xmlWriter.writeAttribute(QLatin1String("max_progress"), QString::number(m_maxProgress)); m_xmlWriter.writeCharacters(QLatin1String("App Transferred")); m_xmlWriter.writeEndElement(); } m_xmlWriter.writeEmptyElement(QLatin1String("app_transfer")); m_xmlWriter.writeAttribute(QLatin1String("status"), (status == IosDeviceManager::Success) ? QLatin1String("SUCCESS") : QLatin1String("FAILURE")); //out.writeCharacters(QString()); // trigger a complete closing of the empty element m_outputFile.flush(); } if (status != IosDeviceManager::Success || --m_operationsRemaining == 0) doExit((status == IosDeviceManager::Success) ? 0 : -1); } void IosTool::didStartApp(const QString &bundlePath, const QString &deviceId, IosDeviceManager::OpStatus status, ServiceConnRef conn, int gdbFd, DeviceSession *deviceSession) { Q_UNUSED(bundlePath) Q_UNUSED(deviceId) { QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeEmptyElement(QLatin1String("app_started")); m_xmlWriter.writeAttribute(QLatin1String("status"), (status == IosDeviceManager::Success) ? QLatin1String("SUCCESS") : QLatin1String("FAILURE")); //out.writeCharacters(QString()); // trigger a complete closing of the empty element m_outputFile.flush(); } if (status != IosDeviceManager::Success || m_requestedOperation == IosDeviceManager::Install) { doExit(); return; } if (gdbFd <= 0) { writeError("no gdb connection"); doExit(-2); return; } if (m_requestedOperation != IosDeviceManager::InstallAndRun && m_requestedOperation != IosDeviceManager::Run) { writeError(QString::fromLatin1("unexpected appOp value %1").arg(m_requestedOperation)); doExit(-3); return; } if (deviceSession) { int qmlPort = deviceSession->qmljsDebugPort(); if (qmlPort) { m_qmlServer = std::make_unique(this, qmlPort, deviceSession); m_qmlServer->startServer(); } } if (m_debug) { m_gdbServer = std::make_unique(this, gdbFd, conn); if (!m_gdbServer->startServer()) { doExit(-4); return; } } { QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeStartElement(QLatin1String("server_ports")); m_xmlWriter.writeAttribute(QLatin1String("gdb_server"), QString::number(m_gdbServer ? m_gdbServer->serverPort() : -1)); m_xmlWriter.writeAttribute(QLatin1String("qml_server"), QString::number(m_qmlServer ? m_qmlServer->serverPort() : -1)); m_xmlWriter.writeEndElement(); m_outputFile.flush(); } if (!m_debug) { m_gdbRunner = std::make_unique(this, conn); // we should not stop the event handling of the main thread // all output moves to the new thread (other option would be to signal it back) QThread *gdbProcessThread = new QThread(); m_gdbRunner->moveToThread(gdbProcessThread); QObject::connect(gdbProcessThread, &QThread::started, m_gdbRunner.get(), &GdbRunner::run); QObject::connect(m_gdbRunner.get(), &GdbRunner::finished, gdbProcessThread, &QThread::quit); QObject::connect(gdbProcessThread, &QThread::finished, gdbProcessThread, &QObject::deleteLater); gdbProcessThread->start(); new std::thread([this]() -> void { readStdin();}); } } void IosTool::writeMsg(const QString &msg) { writeMessageElement(msg, "msg"); } void IosTool::writeMessageElement(const QString &msg, const QString &element) { QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeStartElement(element); writeTextInElement(msg); m_xmlWriter.writeCharacters(QLatin1String("\n")); m_xmlWriter.writeEndElement(); m_outputFile.flush(); } void IosTool::writeMaybeBin(const QString &extraMsg, const char *msg, quintptr len) { char *buf2 = new char[len * 2 + 4]; buf2[0] = '['; const char toHex[] = "0123456789abcdef"; for (quintptr i = 0; i < len; ++i) { buf2[2 * i + 1] = toHex[(0xF & (msg[i] >> 4))]; buf2[2 * i + 2] = toHex[(0xF & msg[i])]; } buf2[2 * len + 1] = ']'; buf2[2 * len + 2] = ' '; buf2[2 * len + 3] = 0; QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeStartElement(QLatin1String("msg")); m_xmlWriter.writeCharacters(extraMsg); m_xmlWriter.writeCharacters(QLatin1String(buf2)); for (quintptr i = 0; i < len; ++i) { if (msg[i] < 0x20 || msg[i] > 0x7f) buf2[i] = '_'; else buf2[i] = msg[i]; } buf2[len] = 0; m_xmlWriter.writeCharacters(QLatin1String(buf2)); delete[] buf2; m_xmlWriter.writeEndElement(); m_outputFile.flush(); } void IosTool::deviceInfo(const QString &deviceId, const IosDeviceManager::Dict &devInfo) { Q_UNUSED(deviceId) { QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeTextElement(QLatin1String("device_id"), deviceId); m_xmlWriter.writeStartElement(QLatin1String("device_info")); for (auto i = devInfo.cbegin(); i != devInfo.cend(); ++i) { m_xmlWriter.writeStartElement(QLatin1String("item")); m_xmlWriter.writeTextElement(QLatin1String("key"), i.key()); m_xmlWriter.writeTextElement(QLatin1String("value"), i.value()); m_xmlWriter.writeEndElement(); } m_xmlWriter.writeEndElement(); m_outputFile.flush(); } doExit(); } void IosTool::writeTextInElement(const QString &output) { const QRegularExpression controlCharRe(QLatin1String("[\x01-\x08]|\x0B|\x0C|[\x0E-\x1F]|\\0000")); int pos = 0; int oldPos = 0; while ((pos = output.indexOf(controlCharRe, pos)) != -1) { QMutexLocker l(&m_xmlMutex); m_xmlWriter.writeCharacters(output.mid(oldPos, pos - oldPos)); m_xmlWriter.writeEmptyElement(QLatin1String("control_char")); m_xmlWriter.writeAttribute(QLatin1String("code"), QString::number(output.at(pos).toLatin1())); pos += 1; oldPos = pos; } m_xmlWriter.writeCharacters(output.mid(oldPos, output.length() - oldPos)); } void IosTool::appOutput(const QString &output) { QMutexLocker l(&m_xmlMutex); if (!m_inAppOutput) m_xmlWriter.writeStartElement(QLatin1String("app_output")); writeTextInElement(output); if (!m_inAppOutput) m_xmlWriter.writeEndElement(); m_outputFile.flush(); } void IosTool::readStdin() { int c = getchar(); if (c == 'k') { QMetaObject::invokeMethod(this, "stopGdbRunner"); writeError(QLatin1String("iostool: Killing inferior.\n")); } else if (c != EOF) { writeError(QLatin1String("iostool: Unexpected character in stdin, stop listening.\n")); } } void IosTool::writeError(const QString &msg) { writeMessageElement(msg, "error"); } void IosTool::stopGdbRunner() { if (m_gdbRunner) { m_gdbRunner->stop(0); QTimer::singleShot(100, this, &IosTool::stopGdbRunner2); } } void IosTool::stopGdbRunner2() { if (m_gdbRunner) m_gdbRunner->stop(1); } void IosTool::stopRelayServers(int errorCode) { if (echoRelays()) writeMsg("gdbServerStops"); if (m_qmlServer) m_qmlServer->stopServer(); if (m_gdbServer) m_gdbServer->stopServer(); doExit(errorCode); } }