1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "iostoolhandler.h"
27 #include "iosconfigurations.h"
28 #include "iosconstants.h"
29 #include "iossimulator.h"
30 #include "simulatorcontrol.h"
31
32 #include <coreplugin/icore.h>
33
34 #include <debugger/debuggerconstants.h>
35
36 #include <utils/fileutils.h>
37 #include <utils/futuresynchronizer.h>
38 #include <utils/qtcassert.h>
39 #include <utils/qtcprocess.h>
40 #include <utils/runextensions.h>
41
42 #include <QCoreApplication>
43 #include <QDir>
44 #include <QFileInfo>
45 #include <QFutureWatcher>
46 #include <QJsonArray>
47 #include <QJsonDocument>
48 #include <QJsonObject>
49 #include <QList>
50 #include <QLoggingCategory>
51 #include <QPointer>
52 #include <QProcess>
53 #include <QProcessEnvironment>
54 #include <QScopedArrayPointer>
55 #include <QSocketNotifier>
56 #include <QTemporaryFile>
57 #include <QTimer>
58 #include <QXmlStreamReader>
59
60 #include <signal.h>
61 #include <string.h>
62 #include <errno.h>
63
64 static Q_LOGGING_CATEGORY(toolHandlerLog, "qtc.ios.toolhandler", QtWarningMsg)
65
66 namespace Ios {
67
68 namespace Internal {
69
70 using namespace std::placeholders;
71
72 // As per the currrent behavior, any absolute path given to simctl --stdout --stderr where the
73 // directory after the root also exists on the simulator's file system will map to
74 // simulator's file system i.e. simctl translates $TMPDIR/somwhere/out.txt to
75 // your_home_dir/Library/Developer/CoreSimulator/Devices/data/$TMP_DIR/somwhere/out.txt.
76 // Because /var also exists on simulator's file system.
77 // Though the log files located at CONSOLE_PATH_TEMPLATE are deleted on
78 // app exit any leftovers shall be removed on simulator restart.
79 static QString CONSOLE_PATH_TEMPLATE = QDir::homePath() +
80 "/Library/Developer/CoreSimulator/Devices/%1/data/tmp/%2";
81
82 class LogTailFiles : public QObject
83 {
84 Q_OBJECT
85 public:
86
exec(QFutureInterface<void> & fi,std::shared_ptr<QTemporaryFile> stdoutFile,std::shared_ptr<QTemporaryFile> stderrFile)87 void exec(QFutureInterface<void> &fi, std::shared_ptr<QTemporaryFile> stdoutFile,
88 std::shared_ptr<QTemporaryFile> stderrFile)
89 {
90 if (fi.isCanceled())
91 return;
92
93 // The future is canceled when app on simulator is stoped.
94 QEventLoop loop;
95 QFutureWatcher<void> watcher;
96 connect(&watcher, &QFutureWatcher<void>::canceled, [&](){
97 loop.quit();
98 });
99 watcher.setFuture(fi.future());
100
101 // Process to print the console output while app is running.
102 auto logProcess = [this, fi](QProcess *tailProcess, std::shared_ptr<QTemporaryFile> file) {
103 QObject::connect(tailProcess, &QProcess::readyReadStandardOutput, [=]() {
104 if (!fi.isCanceled())
105 emit logMessage(QString::fromLocal8Bit(tailProcess->readAll()));
106 });
107 tailProcess->start(QStringLiteral("tail"), {"-f", file->fileName()});
108 };
109
110 auto processDeleter = [](QProcess *process) {
111 if (process->state() != QProcess::NotRunning) {
112 process->terminate();
113 process->waitForFinished();
114 }
115 delete process;
116 };
117
118 std::unique_ptr<QProcess, void(*)(QProcess *)> tailStdout(new QProcess, processDeleter);
119 if (stdoutFile)
120 logProcess(tailStdout.get(), stdoutFile);
121
122 std::unique_ptr<QProcess, void(*)(QProcess *)> tailStderr(new QProcess, processDeleter);
123 if (stderrFile)
124 logProcess(tailStderr.get(), stderrFile);
125
126 // Blocks untill tool is deleted or toolexited is called.
127 loop.exec();
128 }
129
130 signals:
131 void logMessage(QString message);
132 };
133
134 struct ParserState {
135 enum Kind {
136 Msg,
137 DeviceId,
138 Key,
139 Value,
140 QueryResult,
141 AppOutput,
142 ControlChar,
143 AppStarted,
144 InferiorPid,
145 ServerPorts,
146 Item,
147 Status,
148 AppTransfer,
149 DeviceInfo,
150 Exit
151 };
152 Kind kind;
153 QString elName;
154 QString chars;
155 QString key;
156 QString value;
157 QMap<QString,QString> info;
158 int progress = 0, maxProgress = 0;
159 int gdbPort, qmlPort;
collectCharsIos::Internal::ParserState160 bool collectChars() {
161 switch (kind) {
162 case Msg:
163 case DeviceId:
164 case Key:
165 case Value:
166 case Status:
167 case InferiorPid:
168 case AppOutput:
169 return true;
170 case ServerPorts:
171 case QueryResult:
172 case ControlChar:
173 case AppStarted:
174 case AppTransfer:
175 case Item:
176 case DeviceInfo:
177 case Exit:
178 break;
179 }
180 return false;
181 }
182
ParserStateIos::Internal::ParserState183 ParserState(Kind kind) :
184 kind(kind), gdbPort(0), qmlPort(0) { }
185 };
186
187 class IosToolHandlerPrivate
188 {
189 public:
190 explicit IosToolHandlerPrivate(const IosDeviceType &devType, IosToolHandler *q);
191 virtual ~IosToolHandlerPrivate();
192 virtual void requestTransferApp(const QString &bundlePath, const QString &deviceId,
193 int timeout = 1000) = 0;
194 virtual void requestRunApp(const QString &bundlePath, const QStringList &extraArgs,
195 IosToolHandler::RunKind runKind,
196 const QString &deviceId, int timeout = 1000) = 0;
197 virtual void requestDeviceInfo(const QString &deviceId, int timeout = 1000) = 0;
198 virtual bool isRunning() const = 0;
199 virtual void stop(int errorCode) = 0;
200
201 // signals
202 void isTransferringApp(const QString &bundlePath, const QString &deviceId, int progress,
203 int maxProgress, const QString &info);
204 void didTransferApp(const QString &bundlePath, const QString &deviceId,
205 IosToolHandler::OpStatus status);
206 void didStartApp(const QString &bundlePath, const QString &deviceId,
207 IosToolHandler::OpStatus status);
208 void gotServerPorts(const QString &bundlePath, const QString &deviceId, Utils::Port gdbPort,
209 Utils::Port qmlPort);
210 void gotInferiorPid(const QString &bundlePath, const QString &deviceId, qint64 pid);
211 void deviceInfo(const QString &deviceId, const IosToolHandler::Dict &info);
212 void appOutput(const QString &output);
213 void errorMsg(const QString &msg);
214 void toolExited(int code);
215
216 protected:
217 IosToolHandler *q;
218 QString m_deviceId;
219 QString m_bundlePath;
220 IosToolHandler::RunKind m_runKind = IosToolHandler::NormalRun;
221 IosDeviceType m_devType;
222 };
223
224 class IosDeviceToolHandlerPrivate final : public IosToolHandlerPrivate
225 {
226 enum State {
227 NonStarted,
228 Starting,
229 StartedInferior,
230 XmlEndProcessed,
231 Stopped
232 };
233 enum Op {
234 OpNone,
235 OpAppTransfer,
236 OpDeviceInfo,
237 OpAppRun
238 };
239 public:
240 explicit IosDeviceToolHandlerPrivate(const IosDeviceType &devType, IosToolHandler *q);
241 ~IosDeviceToolHandlerPrivate() override;
242
243 // IosToolHandlerPrivate overrides
244 public:
245 void requestTransferApp(const QString &bundlePath, const QString &deviceId,
246 int timeout = 1000) override;
247 void requestRunApp(const QString &bundlePath, const QStringList &extraArgs,
248 IosToolHandler::RunKind runKind,
249 const QString &deviceId, int timeout = 1000) override;
250 void requestDeviceInfo(const QString &deviceId, int timeout = 1000) override;
251 bool isRunning() const override;
252 void start(const QString &exe, const QStringList &args);
253 void stop(int errorCode) override;
254
255 private:
256 void subprocessError(QProcess::ProcessError error);
257 void subprocessFinished(int exitCode, QProcess::ExitStatus exitStatus);
258 void subprocessHasData();
259 void processXml();
260 void killProcess();
261
262 QTimer killTimer;
263 std::shared_ptr<QProcess> process;
264 State state = NonStarted;
265 Op op = OpNone;
266 QXmlStreamReader outputParser;
267 QList<ParserState> stack;
268 };
269
270 /****************************************************************************
271 * Flow to install an app on simulator:-
272 * +------------------+
273 * | Transfer App |
274 * +--------+---------+
275 * |
276 * v
277 * +---------+----------+ +--------------------------------+
278 * | SimulatorRunning +---No------> +SimulatorControl::startSimulator|
279 * +---------+----------+ +--------+-----------------------+
280 * Yes |
281 * | |
282 * v |
283 * +---------+--------------------+ |
284 * | SimulatorControl::installApp | <--------------+
285 * +------------------------------+
286 *
287 *
288 *
289 * Flow to launch an app on Simulator:-
290 * +---------+
291 * | Run App |
292 * +----+----+
293 * |
294 * v
295 * +-------------------+ +----------------------------- - --+
296 * | SimulatorRunning? +---NO------> + SimulatorControl::startSimulator |
297 * +--------+----------+ +----------------+-----------------+
298 * YES |
299 * | |
300 * v |
301 * +---------+------------------------------+ |
302 * | SimulatorControl::launchAppOnSimulator | <-------------+
303 * +----------------------------------------+
304 *
305 ***************************************************************************/
306 class IosSimulatorToolHandlerPrivate : public IosToolHandlerPrivate
307 {
308 public:
309 explicit IosSimulatorToolHandlerPrivate(const IosDeviceType &devType, IosToolHandler *q);
310
311 // IosToolHandlerPrivate overrides
312 public:
313 void requestTransferApp(const QString &appBundlePath, const QString &deviceIdentifier,
314 int timeout = 1000) override;
315 void requestRunApp(const QString &appBundlePath, const QStringList &extraArgs,
316 IosToolHandler::RunKind runKind,
317 const QString &deviceIdentifier, int timeout = 1000) override;
318 void requestDeviceInfo(const QString &deviceId, int timeout = 1000) override;
319 bool isRunning() const override;
320 void stop(int errorCode) override;
321
322 private:
323 void installAppOnSimulator();
324 void launchAppOnSimulator(const QStringList &extraArgs);
325 bool isResponseValid(const SimulatorControl::ResponseData &responseData);
326
327 private:
328 qint64 m_pid = -1;
329 LogTailFiles outputLogger;
330 Utils::FutureSynchronizer futureSynchronizer;
331 };
332
IosToolHandlerPrivate(const IosDeviceType & devType,Ios::IosToolHandler * q)333 IosToolHandlerPrivate::IosToolHandlerPrivate(const IosDeviceType &devType,
334 Ios::IosToolHandler *q) :
335 q(q),
336 m_devType(devType)
337 {
338 }
339
340 IosToolHandlerPrivate::~IosToolHandlerPrivate() = default;
341
342 // signals
isTransferringApp(const QString & bundlePath,const QString & deviceId,int progress,int maxProgress,const QString & info)343 void IosToolHandlerPrivate::isTransferringApp(const QString &bundlePath, const QString &deviceId,
344 int progress, int maxProgress, const QString &info)
345 {
346 emit q->isTransferringApp(q, bundlePath, deviceId, progress, maxProgress, info);
347 }
348
didTransferApp(const QString & bundlePath,const QString & deviceId,Ios::IosToolHandler::OpStatus status)349 void IosToolHandlerPrivate::didTransferApp(const QString &bundlePath, const QString &deviceId,
350 Ios::IosToolHandler::OpStatus status)
351 {
352 emit q->didTransferApp(q, bundlePath, deviceId, status);
353 }
354
didStartApp(const QString & bundlePath,const QString & deviceId,IosToolHandler::OpStatus status)355 void IosToolHandlerPrivate::didStartApp(const QString &bundlePath, const QString &deviceId,
356 IosToolHandler::OpStatus status)
357 {
358 emit q->didStartApp(q, bundlePath, deviceId, status);
359 }
360
gotServerPorts(const QString & bundlePath,const QString & deviceId,Utils::Port gdbPort,Utils::Port qmlPort)361 void IosToolHandlerPrivate::gotServerPorts(const QString &bundlePath, const QString &deviceId,
362 Utils::Port gdbPort, Utils::Port qmlPort)
363 {
364 emit q->gotServerPorts(q, bundlePath, deviceId, gdbPort, qmlPort);
365 }
366
gotInferiorPid(const QString & bundlePath,const QString & deviceId,qint64 pid)367 void IosToolHandlerPrivate::gotInferiorPid(const QString &bundlePath, const QString &deviceId,
368 qint64 pid)
369 {
370 emit q->gotInferiorPid(q, bundlePath, deviceId, pid);
371 }
372
deviceInfo(const QString & deviceId,const Ios::IosToolHandler::Dict & info)373 void IosToolHandlerPrivate::deviceInfo(const QString &deviceId,
374 const Ios::IosToolHandler::Dict &info)
375 {
376 emit q->deviceInfo(q, deviceId, info);
377 }
378
appOutput(const QString & output)379 void IosToolHandlerPrivate::appOutput(const QString &output)
380 {
381 emit q->appOutput(q, output);
382 }
383
errorMsg(const QString & msg)384 void IosToolHandlerPrivate::errorMsg(const QString &msg)
385 {
386 emit q->errorMsg(q, msg);
387 }
388
toolExited(int code)389 void IosToolHandlerPrivate::toolExited(int code)
390 {
391 emit q->toolExited(q, code);
392 }
393
subprocessError(QProcess::ProcessError error)394 void IosDeviceToolHandlerPrivate::subprocessError(QProcess::ProcessError error)
395 {
396 if (state != Stopped)
397 errorMsg(IosToolHandler::tr("iOS tool error %1").arg(error));
398 stop(-1);
399 if (error == QProcess::FailedToStart) {
400 qCDebug(toolHandlerLog) << "IosToolHandler::finished(" << this << ")";
401 emit q->finished(q);
402 }
403 }
404
subprocessFinished(int exitCode,QProcess::ExitStatus exitStatus)405 void IosDeviceToolHandlerPrivate::subprocessFinished(int exitCode, QProcess::ExitStatus exitStatus)
406 {
407 stop((exitStatus == QProcess::NormalExit) ? exitCode : -1 );
408 qCDebug(toolHandlerLog) << "IosToolHandler::finished(" << this << ")";
409 killTimer.stop();
410 emit q->finished(q);
411 }
412
processXml()413 void IosDeviceToolHandlerPrivate::processXml()
414 {
415 while (!outputParser.atEnd()) {
416 QXmlStreamReader::TokenType tt = outputParser.readNext();
417 //qCDebug(toolHandlerLog) << "processXml, tt=" << tt;
418 switch (tt) {
419 case QXmlStreamReader::NoToken:
420 // The reader has not yet read anything.
421 continue;
422 case QXmlStreamReader::Invalid:
423 // An error has occurred, reported in error() and errorString().
424 break;
425 case QXmlStreamReader::StartDocument:
426 // The reader reports the XML version number in documentVersion(), and the encoding
427 // as specified in the XML document in documentEncoding(). If the document is declared
428 // standalone, isStandaloneDocument() returns true; otherwise it returns false.
429 break;
430 case QXmlStreamReader::EndDocument:
431 // The reader reports the end of the document.
432 // state = XmlEndProcessed;
433 break;
434 case QXmlStreamReader::StartElement:
435 // The reader reports the start of an element with namespaceUri() and name(). Empty
436 // elements are also reported as StartElement, followed directly by EndElement.
437 // The convenience function readElementText() can be called to concatenate all content
438 // until the corresponding EndElement. Attributes are reported in attributes(),
439 // namespace declarations in namespaceDeclarations().
440 {
441 const auto elName = outputParser.name();
442 if (elName == QLatin1String("msg")) {
443 stack.append(ParserState(ParserState::Msg));
444 } else if (elName == QLatin1String("exit")) {
445 stack.append(ParserState(ParserState::Exit));
446 toolExited(outputParser.attributes().value(QLatin1String("code"))
447 .toString().toInt());
448 } else if (elName == QLatin1String("device_id")) {
449 stack.append(ParserState(ParserState::DeviceId));
450 } else if (elName == QLatin1String("key")) {
451 stack.append(ParserState(ParserState::Key));
452 } else if (elName == QLatin1String("value")) {
453 stack.append(ParserState(ParserState::Value));
454 } else if (elName == QLatin1String("query_result")) {
455 stack.append(ParserState(ParserState::QueryResult));
456 } else if (elName == QLatin1String("app_output")) {
457 stack.append(ParserState(ParserState::AppOutput));
458 } else if (elName == QLatin1String("control_char")) {
459 QXmlStreamAttributes attributes = outputParser.attributes();
460 QChar c[1] = {QChar::fromLatin1(static_cast<char>(attributes.value(QLatin1String("code")).toString().toInt()))};
461 if (stack.size() > 0 && stack.last().collectChars())
462 stack.last().chars.append(c[0]);
463 stack.append(ParserState(ParserState::ControlChar));
464 break;
465 } else if (elName == QLatin1String("item")) {
466 stack.append(ParserState(ParserState::Item));
467 } else if (elName == QLatin1String("status")) {
468 ParserState pState(ParserState::Status);
469 QXmlStreamAttributes attributes = outputParser.attributes();
470 pState.progress = attributes.value(QLatin1String("progress")).toString().toInt();
471 pState.maxProgress = attributes.value(QLatin1String("max_progress")).toString().toInt();
472 stack.append(pState);
473 } else if (elName == QLatin1String("app_started")) {
474 stack.append(ParserState(ParserState::AppStarted));
475 QXmlStreamAttributes attributes = outputParser.attributes();
476 const auto statusStr = attributes.value(QLatin1String("status"));
477 Ios::IosToolHandler::OpStatus status = Ios::IosToolHandler::Unknown;
478 if (statusStr.compare(QLatin1String("success"), Qt::CaseInsensitive) == 0)
479 status = Ios::IosToolHandler::Success;
480 else if (statusStr.compare(QLatin1String("failure"), Qt::CaseInsensitive) == 0)
481 status = Ios::IosToolHandler::Failure;
482 didStartApp(m_bundlePath, m_deviceId, status);
483 } else if (elName == QLatin1String("app_transfer")) {
484 stack.append(ParserState(ParserState::AppTransfer));
485 QXmlStreamAttributes attributes = outputParser.attributes();
486 const auto statusStr = attributes.value(QLatin1String("status"));
487 Ios::IosToolHandler::OpStatus status = Ios::IosToolHandler::Unknown;
488 if (statusStr.compare(QLatin1String("success"), Qt::CaseInsensitive) == 0)
489 status = Ios::IosToolHandler::Success;
490 else if (statusStr.compare(QLatin1String("failure"), Qt::CaseInsensitive) == 0)
491 status = Ios::IosToolHandler::Failure;
492 didTransferApp(m_bundlePath, m_deviceId, status);
493 } else if (elName == QLatin1String("device_info") || elName == QLatin1String("deviceinfo")) {
494 stack.append(ParserState(ParserState::DeviceInfo));
495 } else if (elName == QLatin1String("inferior_pid")) {
496 stack.append(ParserState(ParserState::InferiorPid));
497 } else if (elName == QLatin1String("server_ports")) {
498 stack.append(ParserState(ParserState::ServerPorts));
499 QXmlStreamAttributes attributes = outputParser.attributes();
500 Utils::Port gdbServerPort(
501 attributes.value(QLatin1String("gdb_server")).toString().toInt());
502 Utils::Port qmlServerPort(
503 attributes.value(QLatin1String("qml_server")).toString().toInt());
504 gotServerPorts(m_bundlePath, m_deviceId, gdbServerPort, qmlServerPort);
505 } else {
506 qCWarning(toolHandlerLog) << "unexpected element " << elName;
507 }
508 break;
509 }
510 case QXmlStreamReader::EndElement:
511 // The reader reports the end of an element with namespaceUri() and name().
512 {
513 ParserState p = stack.last();
514 stack.removeLast();
515 switch (p.kind) {
516 case ParserState::Msg:
517 errorMsg(p.chars);
518 break;
519 case ParserState::DeviceId:
520 if (m_deviceId.isEmpty())
521 m_deviceId = p.chars;
522 else
523 QTC_CHECK(m_deviceId.compare(p.chars, Qt::CaseInsensitive) == 0);
524 break;
525 case ParserState::Key:
526 stack.last().key = p.chars;
527 break;
528 case ParserState::Value:
529 stack.last().value = p.chars;
530 break;
531 case ParserState::Status:
532 isTransferringApp(m_bundlePath, m_deviceId, p.progress, p.maxProgress, p.chars);
533 break;
534 case ParserState::QueryResult:
535 state = XmlEndProcessed;
536 stop(0);
537 return;
538 case ParserState::AppOutput:
539 appOutput(p.chars);
540 break;
541 case ParserState::ControlChar:
542 break;
543 case ParserState::AppStarted:
544 break;
545 case ParserState::AppTransfer:
546 break;
547 case ParserState::Item:
548 stack.last().info.insert(p.key, p.value);
549 break;
550 case ParserState::DeviceInfo:
551 deviceInfo(m_deviceId, p.info);
552 break;
553 case ParserState::Exit:
554 break;
555 case ParserState::InferiorPid:
556 gotInferiorPid(m_bundlePath, m_deviceId, p.chars.toLongLong());
557 break;
558 case ParserState::ServerPorts:
559 break;
560 }
561 break;
562 }
563 case QXmlStreamReader::Characters:
564 // The reader reports characters in text(). If the characters are all white-space,
565 // isWhitespace() returns true. If the characters stem from a CDATA section,
566 // isCDATA() returns true.
567 if (stack.isEmpty())
568 break;
569 if (stack.last().collectChars())
570 stack.last().chars.append(outputParser.text());
571 break;
572 case QXmlStreamReader::Comment:
573 // The reader reports a comment in text().
574 break;
575 case QXmlStreamReader::DTD:
576 // The reader reports a DTD in text(), notation declarations in notationDeclarations(),
577 // and entity declarations in entityDeclarations(). Details of the DTD declaration are
578 // reported in in dtdName(), dtdPublicId(), and dtdSystemId().
579 break;
580 case QXmlStreamReader::EntityReference:
581 // The reader reports an entity reference that could not be resolved. The name of
582 // the reference is reported in name(), the replacement text in text().
583 break;
584 case QXmlStreamReader::ProcessingInstruction:
585 break;
586 }
587 }
588 if (outputParser.hasError()
589 && outputParser.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
590 qCWarning(toolHandlerLog) << "error parsing iosTool output:" << outputParser.errorString();
591 stop(-1);
592 }
593 }
594
killProcess()595 void IosDeviceToolHandlerPrivate::killProcess()
596 {
597 if (isRunning())
598 process->kill();
599 }
600
subprocessHasData()601 void IosDeviceToolHandlerPrivate::subprocessHasData()
602 {
603 qCDebug(toolHandlerLog) << "subprocessHasData, state:" << state;
604 while (true) {
605 switch (state) {
606 case NonStarted:
607 qCWarning(toolHandlerLog) << "IosToolHandler unexpected state in subprocessHasData: NonStarted";
608 Q_FALLTHROUGH();
609 case Starting:
610 case StartedInferior:
611 // read some data
612 {
613 char buf[200];
614 while (isRunning()) {
615 qint64 rRead = process->read(buf, sizeof(buf));
616 if (rRead == -1) {
617 stop(-1);
618 return;
619 }
620 if (rRead == 0)
621 return;
622 qCDebug(toolHandlerLog) << "subprocessHasData read " << QByteArray(buf, rRead);
623 outputParser.addData(QByteArray(buf, rRead));
624 processXml();
625 }
626 break;
627 }
628 case XmlEndProcessed:
629 stop(0);
630 return;
631 case Stopped:
632 return;
633 }
634 }
635 }
636
637 // IosDeviceToolHandlerPrivate
638
IosDeviceToolHandlerPrivate(const IosDeviceType & devType,IosToolHandler * q)639 IosDeviceToolHandlerPrivate::IosDeviceToolHandlerPrivate(const IosDeviceType &devType,
640 IosToolHandler *q)
641 : IosToolHandlerPrivate(devType, q)
642 {
643 killTimer.setSingleShot(true);
644
645 auto deleter = [](QProcess *p) {
646 if (p->state() != QProcess::NotRunning) {
647 p->kill();
648 if (!p->waitForFinished(2000))
649 p->terminate();
650 }
651 delete p;
652 };
653 process = std::shared_ptr<QProcess>(new QProcess, deleter);
654
655 // Prepare & set process Environment.
656 QProcessEnvironment env(QProcessEnvironment::systemEnvironment());
657 foreach (const QString &k, env.keys())
658 if (k.startsWith(QLatin1String("DYLD_")))
659 env.remove(k);
660 QStringList frameworkPaths;
661 const Utils::FilePath libPath = IosConfigurations::developerPath().pathAppended("Platforms/iPhoneSimulator.platform/Developer/Library");
662 for (const auto framework : {"PrivateFrameworks", "OtherFrameworks", "SharedFrameworks"}) {
663 const QString frameworkPath =
664 libPath.pathAppended(QLatin1String(framework)).toFileInfo().canonicalFilePath();
665 if (!frameworkPath.isEmpty())
666 frameworkPaths << frameworkPath;
667 }
668 frameworkPaths << "/System/Library/Frameworks" << "/System/Library/PrivateFrameworks";
669 env.insert(QLatin1String("DYLD_FALLBACK_FRAMEWORK_PATH"), frameworkPaths.join(QLatin1Char(':')));
670 qCDebug(toolHandlerLog) << "IosToolHandler runEnv:" << env.toStringList();
671 process->setProcessEnvironment(env);
672
673 QObject::connect(process.get(), &QProcess::readyReadStandardOutput,
674 std::bind(&IosDeviceToolHandlerPrivate::subprocessHasData,this));
675
676 QObject::connect(process.get(), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
677 std::bind(&IosDeviceToolHandlerPrivate::subprocessFinished,this, _1,_2));
678
679 QObject::connect(process.get(), &QProcess::errorOccurred,
680 std::bind(&IosDeviceToolHandlerPrivate::subprocessError, this, _1));
681
682 QObject::connect(&killTimer, &QTimer::timeout, std::bind(&IosDeviceToolHandlerPrivate::killProcess, this));
683 }
684
~IosDeviceToolHandlerPrivate()685 IosDeviceToolHandlerPrivate::~IosDeviceToolHandlerPrivate()
686 {
687 if (isRunning()) {
688 // Disconnect the signals to avoid notifications while destructing.
689 // QTCREATORBUG-18147
690 process->disconnect();
691 // Quit ios-tool gracefully before kill is executed.
692 process->write("k\n\r");
693 process->closeWriteChannel();
694 // Give some time to ios-tool to finish.
695 process->waitForFinished(2000);
696 }
697 }
698
requestTransferApp(const QString & bundlePath,const QString & deviceId,int timeout)699 void IosDeviceToolHandlerPrivate::requestTransferApp(const QString &bundlePath,
700 const QString &deviceId, int timeout)
701 {
702 m_bundlePath = bundlePath;
703 m_deviceId = deviceId;
704 QStringList args;
705 args << QLatin1String("--id") << deviceId << QLatin1String("--bundle")
706 << bundlePath << QLatin1String("--timeout") << QString::number(timeout)
707 << QLatin1String("--install");
708 start(IosToolHandler::iosDeviceToolPath(), args);
709 }
710
requestRunApp(const QString & bundlePath,const QStringList & extraArgs,IosToolHandler::RunKind runType,const QString & deviceId,int timeout)711 void IosDeviceToolHandlerPrivate::requestRunApp(const QString &bundlePath,
712 const QStringList &extraArgs,
713 IosToolHandler::RunKind runType,
714 const QString &deviceId, int timeout)
715 {
716 m_bundlePath = bundlePath;
717 m_deviceId = deviceId;
718 m_runKind = runType;
719 QStringList args;
720 args << QLatin1String("--id") << deviceId << QLatin1String("--bundle")
721 << bundlePath << QLatin1String("--timeout") << QString::number(timeout);
722 switch (runType) {
723 case IosToolHandler::NormalRun:
724 args << QLatin1String("--run");
725 break;
726 case IosToolHandler::DebugRun:
727 args << QLatin1String("--debug");
728 break;
729 }
730 args << QLatin1String("--args") << extraArgs;
731 op = OpAppRun;
732 start(IosToolHandler::iosDeviceToolPath(), args);
733 }
734
requestDeviceInfo(const QString & deviceId,int timeout)735 void IosDeviceToolHandlerPrivate::requestDeviceInfo(const QString &deviceId, int timeout)
736 {
737 m_deviceId = deviceId;
738 QStringList args;
739 args << QLatin1String("--id") << m_deviceId << QLatin1String("--device-info")
740 << QLatin1String("--timeout") << QString::number(timeout);
741 op = OpDeviceInfo;
742 start(IosToolHandler::iosDeviceToolPath(), args);
743 }
744
isRunning() const745 bool IosDeviceToolHandlerPrivate::isRunning() const
746 {
747 return process && (process->state() != QProcess::NotRunning);
748 }
749
start(const QString & exe,const QStringList & args)750 void IosDeviceToolHandlerPrivate::start(const QString &exe, const QStringList &args)
751 {
752 Q_ASSERT(process);
753 QTC_CHECK(state == NonStarted);
754 state = Starting;
755 qCDebug(toolHandlerLog) << "running " << exe << args;
756 process->start(exe, args);
757 state = StartedInferior;
758 }
759
760
stop(int errorCode)761 void IosDeviceToolHandlerPrivate::stop(int errorCode)
762 {
763 qCDebug(toolHandlerLog) << "IosToolHandlerPrivate::stop";
764 State oldState = state;
765 state = Stopped;
766 switch (oldState) {
767 case NonStarted:
768 qCWarning(toolHandlerLog) << "IosToolHandler::stop() when state was NonStarted";
769 Q_FALLTHROUGH();
770 case Starting:
771 switch (op){
772 case OpNone:
773 qCWarning(toolHandlerLog) << "IosToolHandler::stop() when op was OpNone";
774 break;
775 case OpAppTransfer:
776 didTransferApp(m_bundlePath, m_deviceId, IosToolHandler::Failure);
777 break;
778 case OpAppRun:
779 didStartApp(m_bundlePath, m_deviceId, IosToolHandler::Failure);
780 break;
781 case OpDeviceInfo:
782 break;
783 }
784 Q_FALLTHROUGH();
785 case StartedInferior:
786 case XmlEndProcessed:
787 toolExited(errorCode);
788 break;
789 case Stopped:
790 return;
791 }
792 if (isRunning()) {
793 process->write("k\n\r");
794 process->closeWriteChannel();
795 killTimer.start(1500);
796 }
797 }
798
799
800 // IosSimulatorToolHandlerPrivate
801
IosSimulatorToolHandlerPrivate(const IosDeviceType & devType,IosToolHandler * q)802 IosSimulatorToolHandlerPrivate::IosSimulatorToolHandlerPrivate(const IosDeviceType &devType,
803 IosToolHandler *q)
804 : IosToolHandlerPrivate(devType, q)
805 {
806 QObject::connect(&outputLogger, &LogTailFiles::logMessage,
807 std::bind(&IosToolHandlerPrivate::appOutput, this, _1));
808 futureSynchronizer.setCancelOnWait(true);
809 }
810
requestTransferApp(const QString & appBundlePath,const QString & deviceIdentifier,int timeout)811 void IosSimulatorToolHandlerPrivate::requestTransferApp(const QString &appBundlePath,
812 const QString &deviceIdentifier, int timeout)
813 {
814 Q_UNUSED(timeout)
815 m_bundlePath = appBundlePath;
816 m_deviceId = deviceIdentifier;
817 isTransferringApp(m_bundlePath, m_deviceId, 0, 100, "");
818
819 auto onSimulatorStart = [this](const SimulatorControl::ResponseData &response) {
820 if (!isResponseValid(response))
821 return;
822
823 if (response.success) {
824 installAppOnSimulator();
825 } else {
826 errorMsg(IosToolHandler::tr("Application install on simulator failed. Simulator not running."));
827 didTransferApp(m_bundlePath, m_deviceId, IosToolHandler::Failure);
828 emit q->finished(q);
829 }
830 };
831
832 if (SimulatorControl::isSimulatorRunning(m_deviceId))
833 installAppOnSimulator();
834 else
835 futureSynchronizer.addFuture(
836 Utils::onResultReady(SimulatorControl::startSimulator(m_deviceId), onSimulatorStart));
837 }
838
requestRunApp(const QString & appBundlePath,const QStringList & extraArgs,IosToolHandler::RunKind runType,const QString & deviceIdentifier,int timeout)839 void IosSimulatorToolHandlerPrivate::requestRunApp(const QString &appBundlePath,
840 const QStringList &extraArgs,
841 IosToolHandler::RunKind runType,
842 const QString &deviceIdentifier, int timeout)
843 {
844 Q_UNUSED(timeout)
845 Q_UNUSED(deviceIdentifier)
846 m_bundlePath = appBundlePath;
847 m_deviceId = m_devType.identifier;
848 m_runKind = runType;
849
850 Utils::FilePath appBundle = Utils::FilePath::fromString(m_bundlePath);
851 if (!appBundle.exists()) {
852 errorMsg(IosToolHandler::tr("Application launch on simulator failed. Invalid bundle path %1")
853 .arg(m_bundlePath));
854 didStartApp(m_bundlePath, m_deviceId, Ios::IosToolHandler::Failure);
855 return;
856 }
857
858 auto onSimulatorStart = [this, extraArgs] (const SimulatorControl::ResponseData &response) {
859 if (!isResponseValid(response))
860 return;
861 if (response.success) {
862 launchAppOnSimulator(extraArgs);
863 } else {
864 errorMsg(IosToolHandler::tr("Application launch on simulator failed. Simulator not running."));
865 didStartApp(m_bundlePath, m_deviceId, Ios::IosToolHandler::Failure);
866 }
867 };
868
869 if (SimulatorControl::isSimulatorRunning(m_deviceId))
870 launchAppOnSimulator(extraArgs);
871 else
872 futureSynchronizer.addFuture(
873 Utils::onResultReady(SimulatorControl::startSimulator(m_deviceId), onSimulatorStart));
874 }
875
requestDeviceInfo(const QString & deviceId,int timeout)876 void IosSimulatorToolHandlerPrivate::requestDeviceInfo(const QString &deviceId, int timeout)
877 {
878 Q_UNUSED(timeout)
879 Q_UNUSED(deviceId)
880 }
881
isRunning() const882 bool IosSimulatorToolHandlerPrivate::isRunning() const
883 {
884 #ifdef Q_OS_UNIX
885 return m_pid > 0 && (kill(m_pid, 0) == 0);
886 #else
887 return false;
888 #endif
889 }
890
stop(int errorCode)891 void IosSimulatorToolHandlerPrivate::stop(int errorCode)
892 {
893 #ifdef Q_OS_UNIX
894 if (m_pid > 0)
895 kill(m_pid, SIGKILL);
896 #endif
897 m_pid = -1;
898 futureSynchronizer.cancelAllFutures();
899 futureSynchronizer.flushFinishedFutures();
900
901 toolExited(errorCode);
902 emit q->finished(q);
903 }
904
installAppOnSimulator()905 void IosSimulatorToolHandlerPrivate::installAppOnSimulator()
906 {
907 auto onResponseAppInstall = [this](const SimulatorControl::ResponseData &response) {
908 if (!isResponseValid(response))
909 return;
910
911 if (response.success) {
912 isTransferringApp(m_bundlePath, m_deviceId, 100, 100, "");
913 didTransferApp(m_bundlePath, m_deviceId, IosToolHandler::Success);
914 } else {
915 errorMsg(IosToolHandler::tr("Application install on simulator failed. %1")
916 .arg(response.commandOutput));
917 didTransferApp(m_bundlePath, m_deviceId, IosToolHandler::Failure);
918 }
919 emit q->finished(q);
920 };
921
922 isTransferringApp(m_bundlePath, m_deviceId, 20, 100, "");
923 auto installFuture = SimulatorControl::installApp(m_deviceId,
924 Utils::FilePath::fromString(m_bundlePath));
925 futureSynchronizer.addFuture(Utils::onResultReady(installFuture, onResponseAppInstall));
926 }
927
launchAppOnSimulator(const QStringList & extraArgs)928 void IosSimulatorToolHandlerPrivate::launchAppOnSimulator(const QStringList &extraArgs)
929 {
930 const Utils::FilePath appBundle = Utils::FilePath::fromString(m_bundlePath);
931 const QString bundleId = SimulatorControl::bundleIdentifier(appBundle);
932 const bool debugRun = m_runKind == IosToolHandler::DebugRun;
933 bool captureConsole = IosConfigurations::xcodeVersion() >= QVersionNumber(8);
934 std::shared_ptr<QTemporaryFile> stdoutFile;
935 std::shared_ptr<QTemporaryFile> stderrFile;
936
937 if (captureConsole) {
938 const QString fileTemplate = CONSOLE_PATH_TEMPLATE.arg(m_deviceId).arg(bundleId);
939 stdoutFile.reset(new QTemporaryFile(fileTemplate + ".stdout"));
940 stderrFile.reset(new QTemporaryFile(fileTemplate + ".stderr"));
941
942 captureConsole = stdoutFile->open() && stderrFile->open();
943 if (!captureConsole)
944 errorMsg(IosToolHandler::tr("Cannot capture console output from %1. "
945 "Error redirecting output to %2.*")
946 .arg(bundleId).arg(fileTemplate));
947 } else {
948 errorMsg(IosToolHandler::tr("Cannot capture console output from %1. "
949 "Install Xcode 8 or later.").arg(bundleId));
950 }
951
952 auto monitorPid = [this](QFutureInterface<void> &fi, qint64 pid) {
953 #ifdef Q_OS_UNIX
954 do {
955 // Poll every 1 sec to check whether the app is running.
956 QThread::msleep(1000);
957 } while (!fi.isCanceled() && kill(pid, 0) == 0);
958 #else
959 Q_UNUSED(pid)
960 #endif
961 // Future is cancelled if the app is stopped from the qt creator.
962 if (!fi.isCanceled())
963 stop(0);
964 };
965
966 auto onResponseAppLaunch = [=](const SimulatorControl::ResponseData &response) {
967 if (!isResponseValid(response))
968 return;
969 if (response.success) {
970 m_pid = response.pID;
971 gotInferiorPid(m_bundlePath, m_deviceId, response.pID);
972 didStartApp(m_bundlePath, m_deviceId, Ios::IosToolHandler::Success);
973 // Start monitoring app's life signs.
974 futureSynchronizer.addFuture(Utils::runAsync(monitorPid, response.pID));
975 if (captureConsole)
976 futureSynchronizer.addFuture(Utils::runAsync(&LogTailFiles::exec, &outputLogger,
977 stdoutFile, stderrFile));
978 } else {
979 m_pid = -1;
980 errorMsg(IosToolHandler::tr("Application launch on simulator failed. %1")
981 .arg(response.commandOutput));
982 didStartApp(m_bundlePath, m_deviceId, Ios::IosToolHandler::Failure);
983 stop(-1);
984 emit q->finished(q);
985 }
986 };
987
988 futureSynchronizer.addFuture(
989 Utils::onResultReady(SimulatorControl::launchApp(m_deviceId,
990 bundleId,
991 debugRun,
992 extraArgs,
993 captureConsole ? stdoutFile->fileName()
994 : QString(),
995 captureConsole ? stderrFile->fileName()
996 : QString()),
997 onResponseAppLaunch));
998 }
999
isResponseValid(const SimulatorControl::ResponseData & responseData)1000 bool IosSimulatorToolHandlerPrivate::isResponseValid(const SimulatorControl::ResponseData &responseData)
1001 {
1002 if (responseData.simUdid.compare(m_deviceId) != 0) {
1003 errorMsg(IosToolHandler::tr("Invalid simulator response. Device Id mismatch. "
1004 "Device Id = %1 Response Id = %2")
1005 .arg(responseData.simUdid)
1006 .arg(m_deviceId));
1007 emit q->finished(q);
1008 return false;
1009 }
1010 return true;
1011 }
1012
1013 } // namespace Internal
1014
iosDeviceToolPath()1015 QString IosToolHandler::iosDeviceToolPath()
1016 {
1017 return Core::ICore::libexecPath("ios/iostool").toString();
1018 }
1019
IosToolHandler(const Internal::IosDeviceType & devType,QObject * parent)1020 IosToolHandler::IosToolHandler(const Internal::IosDeviceType &devType, QObject *parent) :
1021 QObject(parent)
1022 {
1023 if (devType.type == Internal::IosDeviceType::IosDevice)
1024 d = new Internal::IosDeviceToolHandlerPrivate(devType, this);
1025 else
1026 d = new Internal::IosSimulatorToolHandlerPrivate(devType, this);
1027 }
1028
~IosToolHandler()1029 IosToolHandler::~IosToolHandler()
1030 {
1031 delete d;
1032 }
1033
stop()1034 void IosToolHandler::stop()
1035 {
1036 d->stop(-1);
1037 }
1038
requestTransferApp(const QString & bundlePath,const QString & deviceId,int timeout)1039 void IosToolHandler::requestTransferApp(const QString &bundlePath, const QString &deviceId,
1040 int timeout)
1041 {
1042 d->requestTransferApp(bundlePath, deviceId, timeout);
1043 }
1044
requestRunApp(const QString & bundlePath,const QStringList & extraArgs,RunKind runType,const QString & deviceId,int timeout)1045 void IosToolHandler::requestRunApp(const QString &bundlePath, const QStringList &extraArgs,
1046 RunKind runType, const QString &deviceId, int timeout)
1047 {
1048 d->requestRunApp(bundlePath, extraArgs, runType, deviceId, timeout);
1049 }
1050
requestDeviceInfo(const QString & deviceId,int timeout)1051 void IosToolHandler::requestDeviceInfo(const QString &deviceId, int timeout)
1052 {
1053 d->requestDeviceInfo(deviceId, timeout);
1054 }
1055
isRunning() const1056 bool IosToolHandler::isRunning() const
1057 {
1058 return d->isRunning();
1059 }
1060
1061 } // namespace Ios
1062
1063 #include "iostoolhandler.moc"
1064