1 /*
2  * command.cpp
3  * Copyright 2011, Jeff Bland <jksb@member.fsf.org>
4  *
5  * This file is part of Tiled.
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along with
18  * this program. If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #include "command.h"
22 
23 #include "actionmanager.h"
24 #include "commandmanager.h"
25 #include "documentmanager.h"
26 #include "logginginterface.h"
27 #include "mapdocument.h"
28 #include "mapobject.h"
29 #include "projectmanager.h"
30 #include "worlddocument.h"
31 #include "worldmanager.h"
32 
33 #include <QAction>
34 #include <QDir>
35 #include <QMessageBox>
36 #include <QProcess>
37 #include <QStandardPaths>
38 #include <QUndoStack>
39 
40 using namespace Tiled;
41 
42 namespace Tiled {
43 
44 class CommandProcess : public QProcess
45 {
46     Q_OBJECT
47 
48 public:
49     CommandProcess(const Command &command, bool inTerminal = false, bool showOutput = true);
50 
51 private:
52     void consoleOutput();
53     void consoleError();
54     void handleProcessError(QProcess::ProcessError);
55 
56     void reportErrorAndDelete(const QString &);
57 
58     QString mName;
59     QString mFinalCommand;
60 
61 #ifdef Q_OS_MAC
62     QTemporaryFile mFile;
63 #endif
64 };
65 
66 
replaceVariables(const QString & string,bool quoteValues=true)67 static QString replaceVariables(const QString &string, bool quoteValues = true)
68 {
69     QString finalString = string;
70     QString replaceString = quoteValues ? QStringLiteral("\"%1\"") :
71                                           QStringLiteral("%1");
72 
73     // Perform variable replacement
74     if (Document *document = DocumentManager::instance()->currentDocument()) {
75         const QString fileName = document->fileName();
76         QFileInfo fileInfo(fileName);
77         const QString mapPath = fileInfo.absolutePath();
78         const QString projectPath = QFileInfo(ProjectManager::instance()->project().fileName()).absolutePath();
79 
80         finalString.replace(QLatin1String("%mapfile"), replaceString.arg(fileName));
81         finalString.replace(QLatin1String("%mappath"), replaceString.arg(mapPath));
82         finalString.replace(QLatin1String("%projectpath"), replaceString.arg(projectPath));
83 
84         if (MapDocument *mapDocument = qobject_cast<MapDocument*>(document)) {
85             if (const Layer *layer = mapDocument->currentLayer()) {
86                 finalString.replace(QLatin1String("%layername"),
87                                     replaceString.arg(layer->name()));
88             }
89         } else if (TilesetDocument *tilesetDocument = qobject_cast<TilesetDocument*>(document)) {
90             QStringList selectedTileIds;
91             for (Tile *tile : tilesetDocument->selectedTiles())
92                 selectedTileIds.append(QString::number(tile->id()));
93 
94             finalString.replace(QLatin1String("%tileid"),
95                                 replaceString.arg(selectedTileIds.join(QLatin1Char(','))));
96         }
97 
98         if (MapObject *currentObject = dynamic_cast<MapObject *>(document->currentObject())) {
99             finalString.replace(QLatin1String("%objecttype"),
100                                 replaceString.arg(currentObject->type()));
101             finalString.replace(QLatin1String("%objectid"),
102                                 replaceString.arg(currentObject->id()));
103         }
104     }
105 
106     return finalString;
107 }
108 
109 } // namespace Tiled
110 
111 
finalWorkingDirectory() const112 QString Command::finalWorkingDirectory() const
113 {
114     QString finalWorkingDirectory = replaceVariables(workingDirectory, false);
115     QString finalExecutable = replaceVariables(executable);
116     QFileInfo mFile(finalExecutable);
117 
118     if (!mFile.exists())
119         mFile = QFileInfo(QStandardPaths::findExecutable(finalExecutable));
120 
121     finalWorkingDirectory.replace(QLatin1String("%executablepath"),
122                                   mFile.absolutePath());
123 
124     return finalWorkingDirectory;
125 }
126 
127 /**
128  * Returns the final command with replaced tokens.
129  */
finalCommand() const130 QString Command::finalCommand() const
131 {
132     QString exe = executable.trimmed();
133 
134     // Quote the executable when not already done, to make it work even when
135     // the path contains spaces.
136     if (!exe.startsWith(QLatin1Char('"')) && !exe.startsWith(QLatin1Char('\'')))
137         exe.prepend(QLatin1Char('"')).append(QLatin1Char('"'));
138 
139     QString finalCommand = QStringLiteral("%1 %2").arg(exe, arguments);
140     return replaceVariables(finalCommand);
141 }
142 
143 /**
144  * Executes the command in the operating system shell or terminal
145  * application.
146  */
execute(bool inTerminal) const147 void Command::execute(bool inTerminal) const
148 {
149     if (saveBeforeExecute) {
150         ActionManager::instance()->action("Save")->trigger();
151 
152         if (Document *document = DocumentManager::instance()->currentDocument()) {
153             const World *world = WorldManager::instance().worldForMap(document->fileName());
154             if (world && WorldManager::instance().saveWorld(world->fileName))
155                 DocumentManager::instance()->ensureWorldDocument(world->fileName)->undoStack()->setClean();
156         }
157     }
158 
159     // Start the process
160     new CommandProcess(*this, inTerminal, showOutput);
161 }
162 
163 /**
164  * Stores this command in a QVariant.
165  */
toVariant() const166 QVariantHash Command::toVariant() const
167 {
168     return QVariantHash {
169         { QStringLiteral("arguments"), arguments },
170         { QStringLiteral("command"), executable },
171         { QStringLiteral("enabled"), isEnabled },
172         { QStringLiteral("name"), name },
173         { QStringLiteral("saveBeforeExecute"), saveBeforeExecute },
174         { QStringLiteral("shortcut"), shortcut },
175         { QStringLiteral("showOutput"), showOutput },
176         { QStringLiteral("workingDirectory"), workingDirectory },
177     };
178 }
179 
180 /**
181  * Generates a command from a QVariant.
182  */
fromVariant(const QVariant & variant)183 Command Command::fromVariant(const QVariant &variant)
184 {
185     const auto hash = variant.toHash();
186 
187     auto read = [&] (const QString &prop) {
188         if (hash.contains(prop))
189             return hash.value(prop);
190 
191         QString oldProp = prop.at(0).toUpper() + prop.mid(1);
192         return hash.value(oldProp);
193     };
194 
195     const QVariant arguments = read(QStringLiteral("arguments"));
196     const QVariant enable = read(QStringLiteral("enabled"));
197     const QVariant executable = read(QStringLiteral("command"));
198     const QVariant name = read(QStringLiteral("name"));
199     const QVariant saveBeforeExecute = read(QStringLiteral("saveBeforeExecute"));
200     const QVariant shortcut = read(QStringLiteral("shortcut"));
201     const QVariant showOutput = read(QStringLiteral("showOutput"));
202     const QVariant workingDirectory = read(QStringLiteral("workingDirectory"));
203 
204     Command command;
205 
206     command.arguments = arguments.toString();
207     command.isEnabled = enable.toBool();
208     command.executable = executable.toString();
209     command.name = name.toString();
210     command.saveBeforeExecute = saveBeforeExecute.toBool();
211     command.shortcut = shortcut.value<QKeySequence>();
212     command.showOutput = showOutput.toBool();
213     command.workingDirectory = workingDirectory.toString();
214 
215     return command;
216 }
217 
CommandProcess(const Command & command,bool inTerminal,bool showOutput)218 CommandProcess::CommandProcess(const Command &command, bool inTerminal, bool showOutput)
219     : QProcess(DocumentManager::instance())
220     , mName(command.name)
221     , mFinalCommand(command.finalCommand())
222 #ifdef Q_OS_MAC
223     , mFile(QDir::tempPath() + QLatin1String("/tiledXXXXXX.command"))
224 #endif
225 {
226     // Give an error if the command is empty or just whitespace
227     if (mFinalCommand.trimmed().isEmpty()) {
228         handleProcessError(QProcess::FailedToStart);
229         return;
230     }
231 
232     // Modify the command to run in a terminal
233     if (inTerminal) {
234 #ifdef Q_OS_LINUX
235         static bool hasGnomeTerminal = QProcess::execute(QLatin1String("which"),
236                                                          QStringList(QLatin1String("gnome-terminal"))) == 0;
237 
238         if (hasGnomeTerminal)
239             mFinalCommand = QLatin1String("gnome-terminal -x ") + mFinalCommand;
240         else
241             mFinalCommand = QLatin1String("xterm -e ") + mFinalCommand;
242 #elif defined(Q_OS_MAC)
243         // The only way I know to launch a Terminal with a command on mac is
244         // to make a .command file and open it. The client command invoke the
245         // executable directly (rather than using open) in order to get std
246         // output in the terminal. Otherwise, you can use the Console
247         // application to see the output.
248 
249         // Create and write the command to a .command file
250 
251         if (!mFile.open()) {
252             reportErrorAndDelete(tr("Unable to create/open %1").arg(mFile.fileName()));
253             return;
254         }
255         mFile.write(mFinalCommand.toLocal8Bit());
256         mFile.close();
257 
258         // Add execute permission to the file
259         int chmodRet = QProcess::execute(QStringLiteral("chmod"),
260                                          { QStringLiteral("+x"), mFile.fileName() });
261         if (chmodRet != 0) {
262             reportErrorAndDelete(tr("Unable to add executable permissions to %1")
263                                  .arg(mFile.fileName()));
264             return;
265         }
266 
267         // Use open command to launch the command in the terminal
268         // -W makes it not return immediately
269         // -n makes it open a new instance of terminal if it is open already
270         mFinalCommand = QStringLiteral("open -W -n \"%1\"")
271                                                          .arg(mFile.fileName());
272 #endif
273     }
274 
275     connect(this, &QProcess::errorOccurred,
276             this, &CommandProcess::handleProcessError);
277 
278     connect(this, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
279             this, &QObject::deleteLater);
280 
281     if (showOutput) {
282         Tiled::INFO(tr("Executing: %1").arg(mFinalCommand));
283 
284         connect(this, &QProcess::readyReadStandardError, this, &CommandProcess::consoleError);
285         connect(this, &QProcess::readyReadStandardOutput, this, &CommandProcess::consoleOutput);
286     }
287 
288     const QString finalWorkingDirectory = command.finalWorkingDirectory();
289     if (!finalWorkingDirectory.trimmed().isEmpty())
290         setWorkingDirectory(finalWorkingDirectory);
291 
292 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
293     start(mFinalCommand);
294 #else
295     QStringList args = QProcess::splitCommand(mFinalCommand);
296     const QString executable = args.takeFirst();
297     start(executable, args);
298 #endif
299 }
300 
consoleOutput()301 void CommandProcess::consoleOutput()
302 {
303     Tiled::INFO(QString::fromLocal8Bit(readAllStandardOutput()));
304 }
305 
consoleError()306 void CommandProcess::consoleError()
307 {
308     Tiled::ERROR(QString::fromLocal8Bit(readAllStandardError()));
309 }
310 
handleProcessError(QProcess::ProcessError error)311 void CommandProcess::handleProcessError(QProcess::ProcessError error)
312 {
313     QString errorStr;
314     switch (error) {
315     case QProcess::FailedToStart:
316         errorStr = tr("The command failed to start.");
317         break;
318     case QProcess::Crashed:
319         errorStr = tr("The command crashed.");
320         break;
321     case QProcess::Timedout:
322         errorStr = tr("The command timed out.");
323         break;
324     default:
325         errorStr = tr("An unknown error occurred.");
326     }
327 
328     reportErrorAndDelete(errorStr);
329 }
330 
reportErrorAndDelete(const QString & error)331 void CommandProcess::reportErrorAndDelete(const QString &error)
332 {
333     const QString title = tr("Error Executing %1").arg(mName);
334     const QString message = error + QLatin1String("\n\n") + mFinalCommand;
335 
336     QWidget *parent = DocumentManager::instance()->widget();
337     QMessageBox::warning(parent, title, message);
338 
339     // Make sure this object gets deleted if the process failed to start
340     deleteLater();
341 }
342 
343 #include "command.moc"
344