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