]> git.lyx.org Git - lyx.git/blob - src/support/Systemcall.cpp
Simplify code
[lyx.git] / src / support / Systemcall.cpp
1 /**
2  * \file Systemcall.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Asger Alstrup
7  * \author Angus Leeming
8  * \author Enrico Forestieri
9  * \author Peter Kuemmel
10  *
11  * Full author contact details are available in file CREDITS.
12  */
13
14 #include <config.h>
15
16 #include "support/debug.h"
17 #include "support/filetools.h"
18 #include "support/gettext.h"
19 #include "support/lstrings.h"
20 #include "support/qstring_helpers.h"
21 #include "support/Systemcall.h"
22 #include "support/SystemcallPrivate.h"
23 #include "support/os.h"
24 #include "support/ProgressInterface.h"
25
26 #include "LyXRC.h"
27
28 #include <cstdlib>
29 #include <iostream>
30
31 #include <QProcess>
32 #include <QTime>
33 #include <QThread>
34 #include <QCoreApplication>
35 #include <QDebug>
36
37 #define USE_QPROCESS
38
39
40 struct Sleep : QThread
41 {
42         static void millisec(unsigned long ms) 
43         {
44                 QThread::usleep(ms * 1000);
45         }
46 };
47
48
49
50
51 using namespace std;
52
53 namespace lyx {
54 namespace support {
55
56
57 class ProgressDummy : public ProgressInterface 
58 {
59 public:
60         ProgressDummy() {}
61
62         void processStarted(QString const &) {}
63         void processFinished(QString const &) {}
64         void appendMessage(QString const &) {}
65         void appendError(QString const &) {}
66         void clearMessages() {}
67         void lyxerrFlush() {}
68
69         void lyxerrConnect() {}
70         void lyxerrDisconnect() {}
71
72         void warning(QString const &, QString const &) {}
73         void toggleWarning(QString const &, QString const &, QString const &) {}
74         void error(QString const &, QString const &, QString const &) {}
75         void information(QString const &, QString const &) {}
76         int prompt(docstring const &, docstring const &, int default_but, int,
77                    docstring const &, docstring const &) { return default_but; }
78 };
79
80
81 static ProgressInterface * progress_instance = 0;
82
83 void ProgressInterface::setInstance(ProgressInterface* p)
84 {
85         progress_instance = p;
86 }
87
88
89 ProgressInterface * ProgressInterface::instance()
90 {
91         if (!progress_instance) {
92                 static ProgressDummy dummy;
93                 return &dummy;
94         }
95         return progress_instance;
96 }
97
98
99
100
101 // Reuse of instance
102 #ifndef USE_QPROCESS
103 int Systemcall::startscript(Starttype how, string const & what,
104                             std::string const & path, bool /*process_events*/)
105 {
106         string command = to_filesystem8bit(from_utf8(latexEnvCmdPrefix(path)))
107                        + commandPrep(what);
108
109         if (how == DontWait) {
110                 switch (os::shell()) {
111                 case os::UNIX:
112                         command += " &";
113                         break;
114                 case os::CMD_EXE:
115                         command = "start /min " + command;
116                         break;
117                 }
118         } else if (os::shell() == os::CMD_EXE)
119                 command = subst(command, "cmd /d /c ", "");
120
121         return ::system(command.c_str());
122 }
123
124 #else
125
126 namespace {
127
128 /*
129  * This is a parser that (mostly) mimics the behavior of a posix shell as
130  * regards quoting, but its output is tailored for being processed by QProcess.
131  * Note that shell metacharacters are not parsed.
132  *
133  * The escape character is the backslash.
134  * A backslash that is not quoted preserves the literal value of the following
135  * character, with the exception of a double-quote '"'. If a double-quote
136  * follows a backslash, it will be replaced by three consecutive double-quotes
137  * (this is how the QProcess parser recognizes a '"' as a simple character
138  * instead of a quoting character). Thus, for example:
139  *     \\  ->  \
140  *     \a  ->  a
141  *     \"  ->  """
142  *
143  * Single-quotes.
144  * Characters enclosed in single-quotes ('') have their literal value preserved.
145  * A single-quote cannot occur within single-quotes. Indeed, a backslash cannot
146  * be used to escape a single-quote in a single-quoted string. In other words,
147  * anything enclosed in single-quotes is passed as is, but the single-quotes
148  * themselves are eliminated. Thus, for example:
149  *    '\'    ->  \
150  *    '\\'   ->  \\
151  *    '\a'   ->  \a
152  *    'a\"b' ->  a\"b
153  *
154  * Double-quotes.
155  * Characters enclosed in double-quotes ("") have their literal value preserved,
156  * with the exception of the backslash. The backslash retains its special
157  * meaning as an escape character only when followed by a double-quote.
158  * Contrarily to the behavior of a posix shell, the double-quotes themselves
159  * are *not* eliminated. Thus, for example:
160  *    "\\"   ->  "\\"
161  *    "\a"   ->  "\a"
162  *    "a\"b" ->  "a"""b"
163  */
164 string const parsecmd(string const & incmd, string & infile, string & outfile,
165                      string & errfile)
166 {
167         bool in_single_quote = false;
168         bool in_double_quote = false;
169         bool escaped = false;
170         string const python_call = "python -tt";
171         vector<string> outcmd(4);
172         size_t start = 0;
173
174         if (prefixIs(incmd, python_call)) {
175                 outcmd[0] = os::python();
176                 start = python_call.length();
177         }
178
179         for (size_t i = start, o = 0; i < incmd.length(); ++i) {
180                 char c = incmd[i];
181                 if (c == '\'') {
182                         if (in_double_quote || escaped) {
183                                 if (in_double_quote && escaped)
184                                         outcmd[o] += '\\';
185                                 outcmd[o] += c;
186                         } else
187                                 in_single_quote = !in_single_quote;
188                         escaped = false;
189                         continue;
190                 }
191                 if (in_single_quote) {
192                         outcmd[o] += c;
193                         continue;
194                 }
195                 if (c == '"') {
196                         if (escaped) {
197                                 // Don't triple double-quotes for redirection
198                                 // files as these won't be parsed by QProcess
199                                 outcmd[o] += string(o ? "\"" : "\"\"\"");
200                                 escaped = false;
201                         } else {
202                                 outcmd[o] += c;
203                                 in_double_quote = !in_double_quote;
204                         }
205                 } else if (c == '\\' && !escaped) {
206                         escaped = !escaped;
207                 } else if (c == '>' && !(in_double_quote || escaped)) {
208                         if (suffixIs(outcmd[o], " 2")) {
209                                 outcmd[o] = rtrim(outcmd[o], "2");
210                                 o = 2;
211                         } else {
212                                 if (suffixIs(outcmd[o], " 1"))
213                                         outcmd[o] = rtrim(outcmd[o], "1");
214                                 o = 1;
215                         }
216                 } else if (c == '<' && !(in_double_quote || escaped)) {
217                         o = 3;
218                 } else {
219                         if (escaped && in_double_quote)
220                                 outcmd[o] += '\\';
221                         outcmd[o] += c;
222                         escaped = false;
223                 }
224         }
225         infile  = trim(outcmd[3], " \"");
226         outfile = trim(outcmd[1], " \"");
227         errfile = trim(outcmd[2], " \"");
228         return trim(outcmd[0]);
229 }
230
231 } // namespace anon
232
233
234
235 int Systemcall::startscript(Starttype how, string const & what,
236                             string const & path, bool process_events)
237 {
238         string const what_ss = commandPrep(what);
239         LYXERR(Debug::INFO,"Running: " << what_ss);
240
241         string infile;
242         string outfile;
243         string errfile;
244         QString const cmd = QString::fromLocal8Bit(
245                         parsecmd(what_ss, infile, outfile, errfile).c_str());
246
247         SystemcallPrivate d(infile, outfile, errfile);
248
249 #ifdef Q_OS_WIN32
250         // QProcess::startDetached cannot provide environment variables. When the
251         // environment variables are set using the latexEnvCmdPrefix and the process
252         // is started with QProcess::startDetached, a console window is shown every 
253         // time a viewer is started. To avoid this, we fall back on Windows to the 
254         // original implementation that creates a QProcess object.
255         d.startProcess(cmd, path, false);
256         if (!d.waitWhile(SystemcallPrivate::Starting, process_events, -1)) {
257                 LYXERR0("Systemcall: '" << cmd << "' did not start!");
258                 LYXERR0("error " << d.errorMessage());
259                 return 10;
260         }
261         if (how == DontWait) {
262                 d.releaseProcess();
263                 return 0;
264         }
265 #else
266         d.startProcess(cmd, path, how == DontWait);
267         if (how == DontWait && d.state == SystemcallPrivate::Running)
268                 return 0;
269
270         if (d.state == SystemcallPrivate::Error
271                         || !d.waitWhile(SystemcallPrivate::Starting, process_events, -1)) {
272                 LYXERR0("Systemcall: '" << cmd << "' did not start!");
273                 LYXERR0("error " << d.errorMessage());
274                 return 10;
275         }
276 #endif
277
278         if (!d.waitWhile(SystemcallPrivate::Running, process_events,
279                          os::timeout_min() * 60 * 1000)) {
280                 LYXERR0("Systemcall: '" << cmd << "' did not finish!");
281                 LYXERR0("error " << d.errorMessage());
282                 LYXERR0("status " << d.exitStatusMessage());
283                 return 20;
284         }
285
286         int const exit_code = d.exitCode();
287         if (exit_code) {
288                 LYXERR0("Systemcall: '" << cmd << "' finished with exit code " << exit_code);
289         }
290
291         return exit_code;
292 }
293
294
295 SystemcallPrivate::SystemcallPrivate(std::string const & sf,
296                                      std::string const & of,
297                                      std::string const & ef) :
298                                 process_(new QProcess), 
299                                 out_index_(0),
300                                 err_index_(0),
301                                 in_file_(sf),
302                                 out_file_(of), 
303                                 err_file_(ef), 
304                                 process_events_(false)
305 {
306         if (!in_file_.empty())
307                 process_->setStandardInputFile(QString::fromLocal8Bit(in_file_.c_str()));
308         if (!out_file_.empty()) {
309                 if (out_file_[0] == '&') {
310                         if (subst(out_file_, " ", "") == "&2"
311                             && err_file_[0] != '&') {
312                                 out_file_ = err_file_;
313                                 process_->setProcessChannelMode(
314                                                 QProcess::MergedChannels);
315                         } else {
316                                 if (err_file_[0] == '&') {
317                                         // Leave alone things such as
318                                         // "1>&2 2>&1". Should not be harmful,
319                                         // but let's give anyway a warning.
320                                         LYXERR0("Unsupported stdout/stderr redirect.");
321                                         err_file_.erase();
322                                 } else {
323                                         LYXERR0("Ambiguous stdout redirect: "
324                                                 << out_file_);
325                                 }
326                                 out_file_ = os::nulldev();
327                         }
328                 }
329                 // Check whether we have to set the output file.
330                 if (out_file_ != os::nulldev()) {
331                         process_->setStandardOutputFile(QString::fromLocal8Bit(
332                                                         out_file_.c_str()));
333                 }
334         }
335         if (!err_file_.empty()) {
336                 if (err_file_[0] == '&') {
337                         if (subst(err_file_, " ", "") == "&1"
338                             && out_file_[0] != '&') {
339                                 process_->setProcessChannelMode(
340                                                 QProcess::MergedChannels);
341                         } else {
342                                 LYXERR0("Ambiguous stderr redirect: "
343                                         << err_file_);
344                         }
345                         // In MergedChannels mode stderr goes to stdout.
346                         err_file_ = os::nulldev();
347                 }
348                 // Check whether we have to set the error file.
349                 if (err_file_ != os::nulldev()) {
350                         process_->setStandardErrorFile(QString::fromLocal8Bit(
351                                                         err_file_.c_str()));
352                 }
353         }
354
355         connect(process_, SIGNAL(readyReadStandardOutput()), SLOT(stdOut()));
356         connect(process_, SIGNAL(readyReadStandardError()), SLOT(stdErr()));
357         connect(process_, SIGNAL(error(QProcess::ProcessError)), SLOT(processError(QProcess::ProcessError)));
358         connect(process_, SIGNAL(started()), this, SLOT(processStarted()));
359         connect(process_, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(processFinished(int, QProcess::ExitStatus)));
360 }
361
362
363 void SystemcallPrivate::startProcess(QString const & cmd, string const & path, bool detached)
364 {
365         cmd_ = cmd;
366         if (detached) {
367                 state = SystemcallPrivate::Running;
368                 if (!QProcess::startDetached(toqstr(latexEnvCmdPrefix(path)) + cmd_)) {
369                         state = SystemcallPrivate::Error;
370                         return;
371                 }
372                 QProcess* released = releaseProcess();
373                 delete released;
374         } else if (process_) {
375                 state = SystemcallPrivate::Starting;
376                 process_->start(toqstr(latexEnvCmdPrefix(path)) + cmd_);
377         }
378 }
379
380
381 void SystemcallPrivate::processEvents()
382 {
383         if (process_events_) {
384                 QCoreApplication::processEvents(/*QEventLoop::ExcludeUserInputEvents*/);
385         }
386 }
387
388
389 void SystemcallPrivate::waitAndProcessEvents()
390 {
391         Sleep::millisec(100);
392         processEvents();
393 }
394
395
396 namespace {
397
398 bool queryStopCommand(QString const & cmd)
399 {
400         docstring text = bformat(_(
401                 "The command\n%1$s\nhas not yet completed.\n\n"
402                 "Do you want to stop it?"), qstring_to_ucs4(cmd));
403         return ProgressInterface::instance()->prompt(_("Stop command?"), text,
404                         1, 1, _("&Stop it"), _("Let it &run")) == 0;
405 }
406
407 }
408
409
410 bool SystemcallPrivate::waitWhile(State waitwhile, bool process_events, int timeout)
411 {
412         if (!process_)
413                 return false;
414
415         bool timedout = false;
416         process_events_ = process_events;
417
418         // Block GUI while waiting,
419         // relay on QProcess' wait functions
420         if (!process_events_) {
421                 if (waitwhile == Starting)
422                         return process_->waitForStarted(timeout);
423                 if (waitwhile == Running) {
424                         int bump = 2;
425                         while (!timedout) {
426                                 if (process_->waitForFinished(timeout))
427                                         return true;
428                                 bool stop = queryStopCommand(cmd_);
429                                 // The command may have finished in the meantime
430                                 if (process_->state() == QProcess::NotRunning)
431                                         return true;
432                                 if (stop) {
433                                         timedout = true;
434                                         process_->kill();
435                                 } else {
436                                         timeout *= bump;
437                                         bump = 3;
438                                 }
439                         }
440                 }
441                 return false;
442         }
443
444         // process events while waiting, no timeout
445         if (timeout == -1) {
446                 while (state == waitwhile && state != Error) {
447                         waitAndProcessEvents();
448                 }
449                 return state != Error;
450         } 
451
452         // process events while waiting with timeout
453         QTime timer;
454         timer.start();
455         while (state == waitwhile && state != Error && !timedout) {
456                 waitAndProcessEvents();
457                 if (timer.elapsed() > timeout) {
458                         bool stop = queryStopCommand(cmd_);
459                         // The command may have finished in the meantime
460                         if (process_->state() == QProcess::NotRunning)
461                                 break;
462                         if (stop) {
463                                 timedout = true;
464                                 process_->kill();
465                         } else
466                                 timeout *= 3;
467                 }
468         }
469         return (state != Error) && !timedout;
470 }
471
472
473 SystemcallPrivate::~SystemcallPrivate()
474 {
475         if (out_index_) {
476                 out_data_[out_index_] = '\0';
477                 out_index_ = 0;
478                 cout << out_data_;
479         }
480         cout.flush();
481         if (err_index_) {
482                 err_data_[err_index_] = '\0';
483                 err_index_ = 0;
484                 cerr << err_data_;
485         }
486         cerr.flush();
487
488         killProcess();
489 }
490
491
492 void SystemcallPrivate::stdOut()
493 {
494         if (process_) {
495                 char c;
496                 process_->setReadChannel(QProcess::StandardOutput);
497                 while (process_->getChar(&c)) {
498                         out_data_[out_index_++] = c;
499                         if (c == '\n' || out_index_ + 1 == buffer_size_) {
500                                 out_data_[out_index_] = '\0';
501                                 out_index_ = 0;
502                                 ProgressInterface::instance()->appendMessage(QString::fromLocal8Bit(out_data_));
503                                 cout << out_data_;
504                         }
505                 }
506         }
507 }
508
509
510 void SystemcallPrivate::stdErr()
511 {
512         if (process_) {
513                 char c;
514                 process_->setReadChannel(QProcess::StandardError);
515                 while (process_->getChar(&c)) {
516                         err_data_[err_index_++] = c;
517                         if (c == '\n' || err_index_ + 1 == buffer_size_) {
518                                 err_data_[err_index_] = '\0';
519                                 err_index_ = 0;
520                                 ProgressInterface::instance()->appendError(QString::fromLocal8Bit(err_data_));
521                                 cerr << err_data_;
522                         }
523                 }
524         }
525 }
526
527
528 void SystemcallPrivate::processStarted()
529 {
530         if (state != Running) {
531                 state = Running;
532                 ProgressInterface::instance()->processStarted(cmd_);
533         }
534 }
535
536
537 void SystemcallPrivate::processFinished(int, QProcess::ExitStatus)
538 {
539         if (state != Finished) {
540                 state = Finished;
541                 ProgressInterface::instance()->processFinished(cmd_);
542         }
543 }
544
545
546 void SystemcallPrivate::processError(QProcess::ProcessError)
547 {
548         state = Error;
549         ProgressInterface::instance()->appendError(errorMessage());
550 }
551
552
553 QString SystemcallPrivate::errorMessage() const 
554 {
555         if (!process_)
556                 return "No QProcess available";
557
558         QString message;
559         switch (process_->error()) {
560                 case QProcess::FailedToStart:
561                         message = "The process failed to start. Either the invoked program is missing, "
562                                       "or you may have insufficient permissions to invoke the program.";
563                         break;
564                 case QProcess::Crashed:
565                         message = "The process crashed some time after starting successfully.";
566                         break;
567                 case QProcess::Timedout:
568                         message = "The process timed out. It might be restarted automatically.";
569                         break;
570                 case QProcess::WriteError:
571                         message = "An error occurred when attempting to write to the process-> For example, "
572                                       "the process may not be running, or it may have closed its input channel.";
573                         break;
574                 case QProcess::ReadError:
575                         message = "An error occurred when attempting to read from the process-> For example, "
576                                       "the process may not be running.";
577                         break;
578                 case QProcess::UnknownError:
579                 default:
580                         message = "An unknown error occurred.";
581                         break;
582         }
583         return message;
584 }
585
586
587 QString SystemcallPrivate::exitStatusMessage() const
588 {
589         if (!process_)
590                 return "No QProcess available";
591
592         QString message;
593         switch (process_->exitStatus()) {
594                 case QProcess::NormalExit:
595                         message = "The process exited normally.";
596                         break;
597                 case QProcess::CrashExit:
598                         message = "The process crashed.";
599                         break;
600                 default:
601                         message = "Unknown exit state.";
602                         break;
603         }
604         return message;
605 }
606
607
608 int SystemcallPrivate::exitCode()
609 {
610         // From Qt's documentation, in regards to QProcess::exitCode(),
611         // "This value is not valid unless exitStatus() returns NormalExit"
612         if (!process_ || process_->exitStatus() != QProcess::NormalExit)
613                 return -1;
614
615         return process_->exitCode();
616 }
617
618
619 QProcess* SystemcallPrivate::releaseProcess()
620 {
621         QProcess* released = process_;
622         process_ = 0;
623         return released;
624 }
625
626
627 void SystemcallPrivate::killProcess()
628 {
629         killProcess(process_);
630 }
631
632
633 void SystemcallPrivate::killProcess(QProcess * p)
634 {
635         if (p) {
636                 p->disconnect();
637                 p->closeReadChannel(QProcess::StandardOutput);
638                 p->closeReadChannel(QProcess::StandardError);
639                 p->close();
640                 delete p;
641         }
642 }
643
644
645
646 #include "moc_SystemcallPrivate.cpp"
647 #endif
648
649 } // namespace support
650 } // namespace lyx