1 | // Copyright (C) 2018 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qmlpreviewapplication.h" |
5 | |
6 | #include <QtCore/QStringList> |
7 | #include <QtCore/QTextStream> |
8 | #include <QtCore/QProcess> |
9 | #include <QtCore/QTimer> |
10 | #include <QtCore/QDateTime> |
11 | #include <QtCore/QFileInfo> |
12 | #include <QtCore/QDebug> |
13 | #include <QtCore/QDir> |
14 | #include <QtCore/QCommandLineParser> |
15 | #include <QtCore/QTemporaryFile> |
16 | #include <QtCore/QUrl> |
17 | #include <QtCore/QLibraryInfo> |
18 | |
19 | QmlPreviewApplication::QmlPreviewApplication(int &argc, char **argv) : |
20 | QCoreApplication(argc, argv), |
21 | m_verbose(false), |
22 | m_connectionAttempts(0) |
23 | { |
24 | m_connection.reset(other: new QQmlDebugConnection); |
25 | m_qmlPreviewClient.reset(other: new QQmlPreviewClient(m_connection.data())); |
26 | m_connectTimer.setInterval(1000); |
27 | |
28 | m_loadTimer.setInterval(100); |
29 | m_loadTimer.setSingleShot(true); |
30 | connect(sender: &m_loadTimer, signal: &QTimer::timeout, context: this, slot: [this]() { |
31 | m_qmlPreviewClient->triggerLoad(url: QUrl()); |
32 | }); |
33 | |
34 | connect(sender: &m_connectTimer, signal: &QTimer::timeout, context: this, slot: &QmlPreviewApplication::tryToConnect); |
35 | connect(sender: m_connection.data(), signal: &QQmlDebugConnection::connected, context: &m_connectTimer, slot: &QTimer::stop); |
36 | |
37 | connect(sender: m_qmlPreviewClient.data(), signal: &QQmlPreviewClient::error, |
38 | context: this, slot: &QmlPreviewApplication::logError); |
39 | connect(sender: m_qmlPreviewClient.data(), signal: &QQmlPreviewClient::request, |
40 | context: this, slot: &QmlPreviewApplication::serveRequest); |
41 | |
42 | connect(sender: &m_watcher, signal: &QmlPreviewFileSystemWatcher::fileChanged, |
43 | context: this, slot: &QmlPreviewApplication::sendFile); |
44 | connect(sender: &m_watcher, signal: &QmlPreviewFileSystemWatcher::directoryChanged, |
45 | context: this, slot: &QmlPreviewApplication::sendDirectory); |
46 | } |
47 | |
48 | QmlPreviewApplication::~QmlPreviewApplication() |
49 | { |
50 | if (m_process && m_process->state() != QProcess::NotRunning) { |
51 | logStatus(status: "Terminating process ..." ); |
52 | m_process->disconnect(); |
53 | m_process->terminate(); |
54 | if (!m_process->waitForFinished(msecs: 1000)) { |
55 | logStatus(status: "Killing process ..." ); |
56 | m_process->kill(); |
57 | } |
58 | } |
59 | } |
60 | |
61 | void QmlPreviewApplication::parseArguments() |
62 | { |
63 | setApplicationName(QLatin1String("qmlpreview" )); |
64 | setApplicationVersion(QLatin1String(qVersion())); |
65 | |
66 | QCommandLineParser parser; |
67 | parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); |
68 | parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsPositionalArguments); |
69 | |
70 | parser.setApplicationDescription(QChar::LineFeed + tr( |
71 | s: "The QML Preview tool watches QML and JavaScript files on disk and updates\n" |
72 | "the application live with any changes. The application to be previewed\n" |
73 | "has to enable QML debugging. See the Qt Creator documentation on how to do\n" |
74 | "this for different Qt versions." )); |
75 | |
76 | QCommandLineOption verbose(QStringList() << QLatin1String("verbose" ), |
77 | tr(s: "Print debugging output." )); |
78 | parser.addOption(commandLineOption: verbose); |
79 | |
80 | parser.addHelpOption(); |
81 | parser.addVersionOption(); |
82 | |
83 | parser.addPositionalArgument(name: QLatin1String("executable" ), |
84 | description: tr(s: "The executable to be started and previewed." ), |
85 | syntax: QLatin1String("[executable]" )); |
86 | parser.addPositionalArgument(name: QLatin1String("parameters" ), |
87 | description: tr(s: "Parameters for the executable to be started." ), |
88 | syntax: QLatin1String("[parameters...]" )); |
89 | |
90 | parser.process(app: *this); |
91 | |
92 | QTemporaryFile file; |
93 | if (file.open()) |
94 | m_socketFile = file.fileName(); |
95 | |
96 | if (parser.isSet(option: verbose)) |
97 | m_verbose = true; |
98 | |
99 | m_arguments = parser.positionalArguments(); |
100 | if (!m_arguments.isEmpty()) |
101 | m_executablePath = m_arguments.takeFirst(); |
102 | |
103 | if (m_executablePath.isEmpty()) { |
104 | logError(error: tr(s: "You have to specify an executable to start." )); |
105 | parser.showHelp(exitCode: 2); |
106 | } |
107 | } |
108 | |
109 | int QmlPreviewApplication::exec() |
110 | { |
111 | QTimer::singleShot(interval: 0, receiver: this, slot: &QmlPreviewApplication::run); |
112 | return QCoreApplication::exec(); |
113 | } |
114 | |
115 | void QmlPreviewApplication::run() |
116 | { |
117 | logStatus(status: QString("Listening on %1 ..." ).arg(a: m_socketFile)); |
118 | m_connection->startLocalServer(fileName: m_socketFile); |
119 | m_process.reset(other: new QProcess(this)); |
120 | QStringList arguments; |
121 | arguments << QString("-qmljsdebugger=file:%1,block,services:QmlPreview" ).arg(a: m_socketFile); |
122 | arguments << m_arguments; |
123 | |
124 | m_process->setProcessChannelMode(QProcess::MergedChannels); |
125 | connect(sender: m_process.data(), signal: &QIODevice::readyRead, |
126 | context: this, slot: &QmlPreviewApplication::processHasOutput); |
127 | connect(sender: m_process.data(), signal: QOverload<int, QProcess::ExitStatus>::of(ptr: &QProcess::finished), |
128 | context: this, slot: [this](int){ processFinished(); }); |
129 | logStatus(status: QString("Starting '%1 %2' ..." ).arg(args&: m_executablePath, args: arguments.join(sep: QLatin1Char(' ')))); |
130 | m_process->start(program: m_executablePath, arguments); |
131 | if (!m_process->waitForStarted()) { |
132 | logError(error: QString("Could not run '%1': %2" ).arg(args&: m_executablePath, args: m_process->errorString())); |
133 | exit(retcode: 1); |
134 | } |
135 | m_connectTimer.start(); |
136 | } |
137 | |
138 | void QmlPreviewApplication::tryToConnect() |
139 | { |
140 | Q_ASSERT(!m_connection->isConnected()); |
141 | ++m_connectionAttempts; |
142 | |
143 | if (m_verbose && !(m_connectionAttempts % 5)) {// print every 5 seconds |
144 | logError(error: QString("No connection received on %1 for %2 seconds ..." ) |
145 | .arg(a: m_socketFile).arg(a: m_connectionAttempts)); |
146 | } |
147 | } |
148 | |
149 | void QmlPreviewApplication::processHasOutput() |
150 | { |
151 | Q_ASSERT(m_process); |
152 | while (m_process->bytesAvailable()) { |
153 | QTextStream out(stderr); |
154 | out << m_process->readAll(); |
155 | } |
156 | } |
157 | |
158 | void QmlPreviewApplication::processFinished() |
159 | { |
160 | Q_ASSERT(m_process); |
161 | int exitCode = 0; |
162 | if (m_process->exitStatus() == QProcess::NormalExit) { |
163 | logStatus(status: QString("Process exited (%1)." ).arg(a: m_process->exitCode())); |
164 | } else { |
165 | logError(error: "Process crashed!" ); |
166 | exitCode = 3; |
167 | } |
168 | exit(retcode: exitCode); |
169 | } |
170 | |
171 | void QmlPreviewApplication::logError(const QString &error) |
172 | { |
173 | QTextStream err(stderr); |
174 | err << "Error: " << error << Qt::endl; |
175 | } |
176 | |
177 | void QmlPreviewApplication::logStatus(const QString &status) |
178 | { |
179 | if (!m_verbose) |
180 | return; |
181 | QTextStream err(stderr); |
182 | err << status << Qt::endl; |
183 | } |
184 | |
185 | void QmlPreviewApplication::serveRequest(const QString &path) |
186 | { |
187 | QFileInfo info(path); |
188 | |
189 | if (info.isDir()) { |
190 | m_qmlPreviewClient->sendDirectory(path, entries: QDir(path).entryList()); |
191 | m_watcher.addDirectory(file: path); |
192 | } else { |
193 | QFile file(path); |
194 | if (file.open(flags: QIODevice::ReadOnly)) { |
195 | m_qmlPreviewClient->sendFile(path, contents: file.readAll()); |
196 | m_watcher.addFile(file: path); |
197 | } else { |
198 | logStatus(status: QString("Could not open file %1 for reading: %2" ).arg(a: path) |
199 | .arg(a: file.errorString())); |
200 | m_qmlPreviewClient->sendError(path); |
201 | } |
202 | } |
203 | } |
204 | |
205 | bool QmlPreviewApplication::sendFile(const QString &path) |
206 | { |
207 | QFile file(path); |
208 | if (file.open(flags: QIODevice::ReadOnly)) { |
209 | m_qmlPreviewClient->sendFile(path, contents: file.readAll()); |
210 | // Defer the Load, because files tend to change multiple times in a row. |
211 | m_loadTimer.start(); |
212 | return true; |
213 | } |
214 | logStatus(status: QString("Could not open file %1 for reading: %2" ).arg(a: path).arg(a: file.errorString())); |
215 | return false; |
216 | } |
217 | |
218 | void QmlPreviewApplication::sendDirectory(const QString &path) |
219 | { |
220 | m_qmlPreviewClient->sendDirectory(path, entries: QDir(path).entryList()); |
221 | m_loadTimer.start(); |
222 | } |
223 | |