1 /*
2     Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3 
4     This file is part of CopyQ.
5 
6     CopyQ is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     CopyQ is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "action.h"
21 
22 #include "common/log.h"
23 #include "common/mimetypes.h"
24 #include "common/processsignals.h"
25 #include "common/timer.h"
26 #include "item/serialize.h"
27 
28 #include <QCoreApplication>
29 #include <QEventLoop>
30 #include <QPointer>
31 #include <QProcessEnvironment>
32 #include <QTimer>
33 
34 #include <cstring>
35 
36 namespace {
37 
startProcess(QProcess * process,const QStringList & args,QIODevice::OpenModeFlag mode)38 void startProcess(QProcess *process, const QStringList &args, QIODevice::OpenModeFlag mode)
39 {
40     QString executable = args.value(0);
41 
42     // Replace "copyq" command with full application path.
43     if (executable == "copyq")
44         executable = QCoreApplication::applicationFilePath();
45 
46     process->start(executable, args.mid(1), mode);
47 }
48 
49 template <typename Entry, typename Container>
appendAndClearNonEmpty(Entry & entry,Container & containter)50 void appendAndClearNonEmpty(Entry &entry, Container &containter)
51 {
52     if ( !entry.isEmpty() ) {
53         containter.append(entry);
54         entry.clear();
55     }
56 }
57 
getScriptFromLabel(const char * label,const QStringRef & cmd,QString * script)58 bool getScriptFromLabel(const char *label, const QStringRef &cmd, QString *script)
59 {
60     if ( cmd.startsWith(label) ) {
61         *script = cmd.string()->mid( cmd.position() + static_cast<int>(strlen(label)) );
62         return true;
63     }
64 
65     return false;
66 }
67 
parseCommands(const QString & cmd,const QStringList & capturedTexts)68 QList< QList<QStringList> > parseCommands(const QString &cmd, const QStringList &capturedTexts)
69 {
70     QList< QList<QStringList> > lines;
71     QList<QStringList> commands;
72     QStringList command;
73     QString script;
74 
75     QString arg;
76     QChar quote;
77     bool escape = false;
78     bool percent = false;
79 
80     for (int i = 0; i < cmd.size(); ++i) {
81         const QChar &c = cmd[i];
82 
83         if (percent) {
84             if (c == '1' || (c >= '2' && c <= '9' && capturedTexts.size() > 1)) {
85                 arg.resize( arg.size() - 1 );
86                 arg.append( capturedTexts.value(c.digitValue() - 1) );
87                 continue;
88             }
89         }
90         percent = !escape && c == '%';
91 
92         if (escape) {
93             escape = false;
94             if (c == 'n') {
95                 arg.append('\n');
96             } else if (c == 't') {
97                 arg.append('\t');
98             } else if (c == '\n') {
99                 // Ignore escaped new line character.
100             } else {
101                 arg.append(c);
102             }
103         } else if (c == '\\') {
104             escape = true;
105         } else if (!quote.isNull()) {
106             if (quote == c) {
107                 quote = QChar();
108                 command.append(arg);
109                 arg.clear();
110             } else {
111                 arg.append(c);
112             }
113         } else if (c == '\'' || c == '"') {
114             quote = c;
115         } else if (c == '|') {
116             appendAndClearNonEmpty(arg, command);
117             appendAndClearNonEmpty(command, commands);
118         } else if (c == '\n' || c == ';') {
119             appendAndClearNonEmpty(arg, command);
120             appendAndClearNonEmpty(command, commands);
121             appendAndClearNonEmpty(commands, lines);
122         } else if ( c.isSpace() ) {
123             if (!arg.isEmpty()) {
124                 command.append(arg);
125                 arg.clear();
126             }
127         } else if ( c == ':' && i + 1 < cmd.size() && cmd[i+1] == '\n' ) {
128             // If there is unescaped colon at the end of a line,
129             // treat the rest of the command as single argument.
130             appendAndClearNonEmpty(arg, command);
131             arg = cmd.mid(i + 2);
132             break;
133         } else {
134             if ( arg.isEmpty() && command.isEmpty() ) {
135                 // Treat command as script if known label is present.
136                 const QStringRef cmd1 = cmd.midRef(i);
137                 if ( getScriptFromLabel("copyq:", cmd1, &script) )
138                     command << "copyq" << "eval" << "--" << script;
139                 else if ( getScriptFromLabel("sh:", cmd1, &script) )
140                     command << "sh" << "-c" << "--" << script << "--";
141                 else if ( getScriptFromLabel("bash:", cmd1, &script) )
142                     command << "bash" << "-c" << "--" << script << "--";
143                 else if ( getScriptFromLabel("perl:", cmd1, &script) )
144                     command << "perl" << "-e" << script << "--";
145                 else if ( getScriptFromLabel("python:", cmd1, &script) )
146                     command << "python" << "-c" << script;
147                 else if ( getScriptFromLabel("ruby:", cmd1, &script) )
148                     command << "ruby" << "-e" << script << "--";
149 
150                 if ( !script.isEmpty() ) {
151                     command.append( capturedTexts.mid(1) );
152                     commands.append(command);
153                     lines.append(commands);
154                     return lines;
155                 }
156             }
157 
158             arg.append(c);
159         }
160     }
161 
162     appendAndClearNonEmpty(arg, command);
163     appendAndClearNonEmpty(command, commands);
164     appendAndClearNonEmpty(commands, lines);
165 
166     return lines;
167 }
168 
169 template <typename Iterator>
pipeThroughProcesses(Iterator begin,Iterator end)170 void pipeThroughProcesses(Iterator begin, Iterator end)
171 {
172     auto it1 = begin;
173     for (auto it2 = it1 + 1; it2 != end; it1 = it2++) {
174         (*it1)->setStandardOutputProcess(*it2);
175         connectProcessFinished(*it2, *it1, &QProcess::terminate);
176     }
177 }
178 
179 } // namespace
180 
terminateProcess(QProcess * p)181 void terminateProcess(QProcess *p)
182 {
183     if (p->state() == QProcess::NotRunning)
184         return;
185 
186     p->terminate();
187     if ( p->state() != QProcess::NotRunning && !p->waitForFinished(5000) ) {
188         p->kill();
189         p->waitForFinished(5000);
190     }
191 }
192 
Action(QObject * parent)193 Action::Action(QObject *parent)
194     : QObject(parent)
195     , m_failed(false)
196     , m_currentLine(-1)
197     , m_exitCode(0)
198 {
199 }
200 
~Action()201 Action::~Action()
202 {
203     closeSubCommands();
204 }
205 
commandLine() const206 QString Action::commandLine() const
207 {
208     QString text;
209     for ( const auto &line : m_cmds ) {
210         for ( const auto &args : line ) {
211             if ( !text.isEmpty() )
212                 text.append(QChar('|'));
213             text.append(args.join(" "));
214         }
215         text.append('\n');
216     }
217     return text.trimmed();
218 }
219 
setCommand(const QString & command,const QStringList & arguments)220 void Action::setCommand(const QString &command, const QStringList &arguments)
221 {
222     m_cmds = parseCommands(command, arguments);
223 }
224 
setCommand(const QStringList & arguments)225 void Action::setCommand(const QStringList &arguments)
226 {
227     m_cmds.clear();
228     m_cmds.append(QList<QStringList>() << arguments);
229 }
230 
setInputWithFormat(const QVariantMap & data,const QString & inputFormat)231 void Action::setInputWithFormat(const QVariantMap &data, const QString &inputFormat)
232 {
233     if (inputFormat == mimeItems) {
234         m_input = serializeData(data);
235         m_inputFormats = data.keys();
236     } else {
237         m_input = data.value(inputFormat).toByteArray();
238         m_inputFormats = QStringList(inputFormat);
239     }
240 }
241 
start()242 void Action::start()
243 {
244     closeSubCommands();
245 
246     if ( m_currentLine + 1 >= m_cmds.size() ) {
247         finish();
248         return;
249     }
250 
251     ++m_currentLine;
252     const QList<QStringList> &cmds = m_cmds[m_currentLine];
253 
254     Q_ASSERT( !cmds.isEmpty() );
255 
256     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
257     if (m_id != -1)
258         env.insert("COPYQ_ACTION_ID", QString::number(m_id));
259     if ( !m_name.isEmpty() )
260         env.insert("COPYQ_ACTION_NAME", m_name);
261 
262     for (int i = 0; i < cmds.size(); ++i) {
263         auto process = new QProcess(this);
264         m_processes.push_back(process);
265         process->setProcessEnvironment(env);
266         if ( !m_workingDirectoryPath.isEmpty() )
267             process->setWorkingDirectory(m_workingDirectoryPath);
268 
269         connectProcessError(process, this, &Action::onSubProcessError);
270         connect( process, &QProcess::readyReadStandardError,
271                  this, &Action::onSubProcessErrorOutput );
272     }
273 
274     pipeThroughProcesses(m_processes.begin(), m_processes.end());
275 
276     QProcess *lastProcess = m_processes.back();
277     connect( lastProcess, &QProcess::started,
278              this, &Action::onSubProcessStarted );
279     connectProcessFinished( lastProcess, this, &Action::onSubProcessFinished );
280     connect( lastProcess, &QProcess::readyReadStandardOutput,
281              this, &Action::onSubProcessOutput );
282 
283     // Writing directly to stdin of a process on Windows can hang the app.
284     QProcess *firstProcess = m_processes.front();
285     connect( firstProcess, &QProcess::started,
286              this, &Action::writeInput, Qt::QueuedConnection );
287     connect( firstProcess, &QProcess::bytesWritten,
288              this, &Action::onBytesWritten, Qt::QueuedConnection );
289 
290     const bool needWrite = !m_input.isEmpty();
291     if (m_processes.size() == 1) {
292         const auto mode =
293                 (needWrite && m_readOutput) ? QIODevice::ReadWrite
294               : needWrite ? QIODevice::WriteOnly
295               : m_readOutput ? QIODevice::ReadOnly
296               : QIODevice::NotOpen;
297         startProcess(firstProcess, cmds.first(), mode);
298     } else {
299         auto it = m_processes.begin();
300         auto cmdIt = cmds.constBegin();
301         startProcess(*it, *cmdIt, needWrite ? QIODevice::ReadWrite : QIODevice::ReadOnly);
302         for (++it, ++cmdIt; it != m_processes.end() - 1; ++it, ++cmdIt)
303             startProcess(*it, *cmdIt, QIODevice::ReadWrite);
304         startProcess(lastProcess, cmds.last(), m_readOutput ? QIODevice::ReadWrite : QIODevice::WriteOnly);
305     }
306 }
307 
waitForFinished(int msecs)308 bool Action::waitForFinished(int msecs)
309 {
310     if ( !isRunning() )
311         return true;
312 
313     QPointer<QObject> self(this);
314     QEventLoop loop;
315     QTimer t;
316     connect(this, &Action::actionFinished, &loop, &QEventLoop::quit);
317     if (msecs >= 0) {
318         connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit);
319         t.setSingleShot(true);
320         t.start(msecs);
321     }
322     loop.exec(QEventLoop::ExcludeUserInputEvents);
323 
324     // Loop stopped because application is exiting?
325     while ( self && isRunning() && (msecs < 0 || t.isActive()) )
326         QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents, 10);
327 
328     return !self || !isRunning();
329 }
330 
isRunning() const331 bool Action::isRunning() const
332 {
333     return !m_processes.empty() && m_processes.back()->state() != QProcess::NotRunning;
334 }
335 
setData(const QVariantMap & data)336 void Action::setData(const QVariantMap &data)
337 {
338     m_data = data;
339 }
340 
data() const341 const QVariantMap &Action::data() const
342 {
343     return m_data;
344 }
345 
appendOutput(const QByteArray & output)346 void Action::appendOutput(const QByteArray &output)
347 {
348     if ( !output.isEmpty() )
349         emit actionOutput(output);
350 }
351 
appendErrorOutput(const QByteArray & errorOutput)352 void Action::appendErrorOutput(const QByteArray &errorOutput)
353 {
354     m_errorOutput.append(errorOutput);
355 }
356 
onSubProcessError(QProcess::ProcessError error)357 void Action::onSubProcessError(QProcess::ProcessError error)
358 {
359     QProcess *p = qobject_cast<QProcess*>(sender());
360     Q_ASSERT(p);
361 
362     // Ignore write-to-process error, process can ignore the input.
363     if (error != QProcess::WriteError) {
364         if (!m_errorString.isEmpty())
365             m_errorString.append("\n");
366         m_errorString.append( p->errorString() );
367         m_failed = true;
368     }
369 
370     if ( !isRunning() )
371         finish();
372 }
373 
onSubProcessStarted()374 void Action::onSubProcessStarted()
375 {
376     if (m_currentLine == 0)
377         emit actionStarted(this);
378 }
379 
onSubProcessFinished()380 void Action::onSubProcessFinished()
381 {
382     onSubProcessOutput();
383     start();
384 }
385 
onSubProcessOutput()386 void Action::onSubProcessOutput()
387 {
388     if ( m_processes.empty() )
389         return;
390 
391     auto p = m_processes.back();
392     if ( p->isReadable() )
393         appendOutput( p->readAll() );
394 }
395 
onSubProcessErrorOutput()396 void Action::onSubProcessErrorOutput()
397 {
398     QProcess *p = qobject_cast<QProcess*>(sender());
399     Q_ASSERT(p);
400 
401     if ( p->isReadable() )
402         appendErrorOutput( p->readAllStandardError() );
403 }
404 
writeInput()405 void Action::writeInput()
406 {
407     if (m_processes.empty())
408         return;
409 
410     QProcess *p = m_processes.front();
411 
412     if (m_input.isEmpty())
413         p->closeWriteChannel();
414     else
415         p->write(m_input);
416 }
417 
onBytesWritten()418 void Action::onBytesWritten()
419 {
420     if ( !m_processes.empty() )
421         m_processes.front()->closeWriteChannel();
422 }
423 
terminate()424 void Action::terminate()
425 {
426     if (m_processes.empty())
427         return;
428 
429     for (auto p : m_processes)
430         p->terminate();
431 
432     waitForFinished(5000);
433     for (auto p : m_processes)
434         terminateProcess(p);
435 }
436 
closeSubCommands()437 void Action::closeSubCommands()
438 {
439     terminate();
440 
441     if (m_processes.empty())
442         return;
443 
444     m_exitCode = m_processes.back()->exitCode();
445     m_failed = m_failed || m_processes.back()->exitStatus() != QProcess::NormalExit;
446 
447     for (auto p : m_processes)
448         p->deleteLater();
449 
450     m_processes.clear();
451 }
452 
finish()453 void Action::finish()
454 {
455     closeSubCommands();
456     emit actionFinished(this);
457 }
458