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 "externaleditors.h"
27
28 #include <utils/algorithm.h>
29 #include <utils/hostosinfo.h>
30 #include <utils/qtcprocess.h>
31 #include <projectexplorer/project.h>
32 #include <projectexplorer/projectexplorer.h>
33 #include <projectexplorer/projectexplorerconstants.h>
34 #include <projectexplorer/target.h>
35 #include <projectexplorer/session.h>
36 #include <qtsupport/qtkitinformation.h>
37 #include <designer/designerconstants.h>
38
39 #include <QProcess>
40 #include <QDebug>
41
42 #include <QTcpSocket>
43 #include <QTcpServer>
44
45 using namespace ProjectExplorer;
46 using namespace Utils;
47
48 enum { debug = 0 };
49
50 namespace QmakeProjectManager {
51 namespace Internal {
52
53 // ------------ Messages
msgStartFailed(const QString & binary,QStringList arguments)54 static inline QString msgStartFailed(const QString &binary, QStringList arguments)
55 {
56 arguments.push_front(binary);
57 return ExternalQtEditor::tr("Unable to start \"%1\"").arg(arguments.join(QLatin1Char(' ')));
58 }
59
msgAppNotFound(const QString & id)60 static inline QString msgAppNotFound(const QString &id)
61 {
62 return ExternalQtEditor::tr("The application \"%1\" could not be found.").arg(id);
63 }
64
65 // -- Commands and helpers
linguistBinary(const QtSupport::BaseQtVersion * qtVersion)66 static QString linguistBinary(const QtSupport::BaseQtVersion *qtVersion)
67 {
68 if (qtVersion)
69 return qtVersion->linguistFilePath().toString();
70 return QLatin1String(Utils::HostOsInfo::isMacHost() ? "Linguist" : "linguist");
71 }
72
designerBinary(const QtSupport::BaseQtVersion * qtVersion)73 static QString designerBinary(const QtSupport::BaseQtVersion *qtVersion)
74 {
75 if (qtVersion)
76 return qtVersion->designerFilePath().toString();
77 return QLatin1String(Utils::HostOsInfo::isMacHost() ? "Designer" : "designer");
78 }
79
80 // Mac: Change the call 'Foo.app/Contents/MacOS/Foo <filelist>' to
81 // 'open -a Foo.app <filelist>'. doesn't support generic command line arguments
createMacOpenCommand(const ExternalQtEditor::LaunchData & data)82 static ExternalQtEditor::LaunchData createMacOpenCommand(const ExternalQtEditor::LaunchData &data)
83 {
84 ExternalQtEditor::LaunchData openData = data;
85 const int appFolderIndex = data.binary.lastIndexOf(QLatin1String("/Contents/MacOS/"));
86 if (appFolderIndex != -1) {
87 openData.binary = "open";
88 openData.arguments = QStringList({QString("-a"), data.binary.left(appFolderIndex)})
89 + data.arguments;
90 }
91 return openData;
92 }
93
94 static const char designerIdC[] = "Qt.Designer";
95 static const char linguistIdC[] = "Qt.Linguist";
96
97 static const char designerDisplayName[] = QT_TRANSLATE_NOOP("OpenWith::Editors", "Qt Designer");
98 static const char linguistDisplayName[] = QT_TRANSLATE_NOOP("OpenWith::Editors", "Qt Linguist");
99
100 // -------------- ExternalQtEditor
ExternalQtEditor(Utils::Id id,const QString & displayName,const QString & mimetype,const CommandForQtVersion & commandForQtVersion)101 ExternalQtEditor::ExternalQtEditor(Utils::Id id,
102 const QString &displayName,
103 const QString &mimetype,
104 const CommandForQtVersion &commandForQtVersion) :
105 m_mimeTypes(mimetype),
106 m_id(id),
107 m_displayName(displayName),
108 m_commandForQtVersion(commandForQtVersion)
109 {
110 }
111
createLinguistEditor()112 ExternalQtEditor *ExternalQtEditor::createLinguistEditor()
113 {
114 return new ExternalQtEditor(linguistIdC,
115 QLatin1String(linguistDisplayName),
116 QLatin1String(ProjectExplorer::Constants::LINGUIST_MIMETYPE),
117 linguistBinary);
118 }
119
createDesignerEditor()120 ExternalQtEditor *ExternalQtEditor::createDesignerEditor()
121 {
122 if (Utils::HostOsInfo::isMacHost()) {
123 return new ExternalQtEditor(designerIdC,
124 QLatin1String(designerDisplayName),
125 QLatin1String(ProjectExplorer::Constants::FORM_MIMETYPE),
126 designerBinary);
127 } else {
128 return new DesignerExternalEditor;
129 }
130 }
131
mimeTypes() const132 QStringList ExternalQtEditor::mimeTypes() const
133 {
134 return m_mimeTypes;
135 }
136
id() const137 Utils::Id ExternalQtEditor::id() const
138 {
139 return m_id;
140 }
141
displayName() const142 QString ExternalQtEditor::displayName() const
143 {
144 return m_displayName;
145 }
146
findFirstCommand(QVector<QtSupport::BaseQtVersion * > qtVersions,ExternalQtEditor::CommandForQtVersion command)147 static QString findFirstCommand(QVector<QtSupport::BaseQtVersion *> qtVersions,
148 ExternalQtEditor::CommandForQtVersion command)
149 {
150 foreach (QtSupport::BaseQtVersion *qt, qtVersions) {
151 if (qt) {
152 const QString binary = command(qt);
153 if (!binary.isEmpty())
154 return binary;
155 }
156 }
157 return QString();
158 }
159
getEditorLaunchData(const Utils::FilePath & filePath,LaunchData * data,QString * errorMessage) const160 bool ExternalQtEditor::getEditorLaunchData(const Utils::FilePath &filePath,
161 LaunchData *data,
162 QString *errorMessage) const
163 {
164 // Check in order for Qt version with the binary:
165 // - active kit of project
166 // - any other of the project
167 // - default kit
168 // - any other kit
169 // As fallback check PATH
170 data->workingDirectory.clear();
171 QVector<QtSupport::BaseQtVersion *> qtVersionsToCheck; // deduplicated after being filled
172 if (const Project *project = SessionManager::projectForFile(filePath)) {
173 data->workingDirectory = project->projectDirectory().toString();
174 // active kit
175 if (const Target *target = project->activeTarget()) {
176 qtVersionsToCheck << QtSupport::QtKitAspect::qtVersion(target->kit());
177 }
178 // all kits of project
179 qtVersionsToCheck += Utils::transform<QVector>(project->targets(), [](Target *t) {
180 return QTC_GUARD(t) ? QtSupport::QtKitAspect::qtVersion(t->kit()) : nullptr;
181 });
182 }
183 // default kit
184 qtVersionsToCheck << QtSupport::QtKitAspect::qtVersion(KitManager::defaultKit());
185 // all kits
186 qtVersionsToCheck += Utils::transform<QVector>(KitManager::kits(), QtSupport::QtKitAspect::qtVersion);
187 qtVersionsToCheck = Utils::filteredUnique(qtVersionsToCheck); // can still contain nullptr
188 data->binary = findFirstCommand(qtVersionsToCheck, m_commandForQtVersion);
189 // fallback
190 if (data->binary.isEmpty())
191 data->binary = Utils::QtcProcess::locateBinary(m_commandForQtVersion(nullptr));
192 if (data->binary.isEmpty()) {
193 *errorMessage = msgAppNotFound(id().toString());
194 return false;
195 }
196 // Setup binary + arguments, use Mac Open if appropriate
197 data->arguments.push_back(filePath.toString());
198 if (Utils::HostOsInfo::isMacHost())
199 *data = createMacOpenCommand(*data);
200 if (debug)
201 qDebug() << Q_FUNC_INFO << '\n' << data->binary << data->arguments;
202 return true;
203 }
204
startEditor(const Utils::FilePath & filePath,QString * errorMessage)205 bool ExternalQtEditor::startEditor(const Utils::FilePath &filePath, QString *errorMessage)
206 {
207 LaunchData data;
208 return getEditorLaunchData(filePath, &data, errorMessage)
209 && startEditorProcess(data, errorMessage);
210 }
211
startEditorProcess(const LaunchData & data,QString * errorMessage)212 bool ExternalQtEditor::startEditorProcess(const LaunchData &data, QString *errorMessage)
213 {
214 if (debug)
215 qDebug() << Q_FUNC_INFO << '\n' << data.binary << data.arguments << data.workingDirectory;
216 qint64 pid = 0;
217 if (!QProcess::startDetached(data.binary, data.arguments, data.workingDirectory, &pid)) {
218 *errorMessage = msgStartFailed(data.binary, data.arguments);
219 return false;
220 }
221 return true;
222 }
223
224 // --------------- DesignerExternalEditor with Designer Tcp remote control.
DesignerExternalEditor()225 DesignerExternalEditor::DesignerExternalEditor() :
226 ExternalQtEditor(designerIdC,
227 QLatin1String(designerDisplayName),
228 QLatin1String(Designer::Constants::FORM_MIMETYPE),
229 designerBinary)
230 {
231 }
232
processTerminated(const QString & binary)233 void DesignerExternalEditor::processTerminated(const QString &binary)
234 {
235 const ProcessCache::iterator it = m_processCache.find(binary);
236 if (it == m_processCache.end())
237 return;
238 // Make sure socket is closed and cleaned, remove from cache
239 QTcpSocket *socket = it.value();
240 m_processCache.erase(it); // Note that closing will cause the slot to be retriggered
241 if (debug)
242 qDebug() << Q_FUNC_INFO << '\n' << binary << socket->state();
243 if (socket->state() == QAbstractSocket::ConnectedState)
244 socket->close();
245 socket->deleteLater();
246 }
247
startEditor(const Utils::FilePath & filePath,QString * errorMessage)248 bool DesignerExternalEditor::startEditor(const Utils::FilePath &filePath, QString *errorMessage)
249 {
250 LaunchData data;
251 // Find the editor binary
252 if (!getEditorLaunchData(filePath, &data, errorMessage)) {
253 return false;
254 }
255 // Known one?
256 const ProcessCache::iterator it = m_processCache.find(data.binary);
257 if (it != m_processCache.end()) {
258 // Process is known, write to its socket to cause it to open the file
259 if (debug)
260 qDebug() << Q_FUNC_INFO << "\nWriting to socket:" << data.binary << filePath;
261 QTcpSocket *socket = it.value();
262 if (!socket->write(filePath.toString().toUtf8() + '\n')) {
263 *errorMessage = tr("Qt Designer is not responding (%1).").arg(socket->errorString());
264 return false;
265 }
266 return true;
267 }
268 // No process yet. Create socket & launch the process
269 QTcpServer server;
270 if (!server.listen(QHostAddress::LocalHost)) {
271 *errorMessage = tr("Unable to create server socket: %1").arg(server.errorString());
272 return false;
273 }
274 const quint16 port = server.serverPort();
275 if (debug)
276 qDebug() << Q_FUNC_INFO << "\nLaunching server:" << port << data.binary << filePath;
277 // Start first one with file and socket as '-client port file'
278 // Wait for the socket listening
279 data.arguments.push_front(QString::number(port));
280 data.arguments.push_front(QLatin1String("-client"));
281
282 if (!startEditorProcess(data, errorMessage))
283 return false;
284 // Insert into cache if socket is created, else try again next time
285 if (server.waitForNewConnection(3000)) {
286 QTcpSocket *socket = server.nextPendingConnection();
287 socket->setParent(this);
288 const QString binary = data.binary;
289 m_processCache.insert(binary, socket);
290 auto mapSlot = [this, binary] { processTerminated(binary); };
291 connect(socket, &QAbstractSocket::disconnected, this, mapSlot);
292 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
293 const auto errorOccurred = QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error);
294 #else
295 const auto errorOccurred = &QAbstractSocket::errorOccurred;
296 #endif
297 connect(socket, errorOccurred, this, mapSlot);
298 }
299 return true;
300 }
301
302 } // namespace Internal
303 } // namespace QmakeProjectManager
304