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