X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Fsupport%2FSystemcall.cpp;h=cc2c3814ab761c17ec360316f1b071e7ea5e21aa;hb=28be7d552f62cc02fa86d7f79201d089bfb2d7b5;hp=20813e794876acb2d9322e67c14cf25d1f4562f6;hpb=c73eb3497aa3e34b2536cf44071053d30d5c5166;p=lyx.git diff --git a/src/support/Systemcall.cpp b/src/support/Systemcall.cpp index 20813e7948..cc2c3814ab 100644 --- a/src/support/Systemcall.cpp +++ b/src/support/Systemcall.cpp @@ -4,9 +4,9 @@ * Licence details can be found in the file COPYING. * * \author Asger Alstrup - * - * Interface cleaned up by * \author Angus Leeming + * \author Enrico Forestieri + * \author Peter Kuemmel * * Full author contact details are available in file CREDITS. */ @@ -14,36 +14,100 @@ #include #include "support/debug.h" +#include "support/filetools.h" +#include "support/gettext.h" +#include "support/lstrings.h" #include "support/qstring_helpers.h" #include "support/Systemcall.h" +#include "support/SystemcallPrivate.h" #include "support/os.h" +#include "support/ProgressInterface.h" + +#include "LyX.h" +#include "LyXRC.h" #include #include #include +#include +#include +#include +#include #define USE_QPROCESS + +struct Sleep : QThread +{ + static void millisec(unsigned long ms) + { + QThread::usleep(ms * 1000); + } +}; + + + + using namespace std; namespace lyx { namespace support { -static void killProcess(QProcess * p) + +class ProgressDummy : public ProgressInterface +{ +public: + ProgressDummy() {} + + void processStarted(QString const &) {} + void processFinished(QString const &) {} + void appendMessage(QString const &) {} + void appendError(QString const &) {} + void clearMessages() {} + void lyxerrFlush() {} + + void lyxerrConnect() {} + void lyxerrDisconnect() {} + + void warning(QString const &, QString const &) {} + void toggleWarning(QString const &, QString const &, QString const &) {} + void error(QString const &, QString const &, QString const &) {} + void information(QString const &, QString const &) {} + int prompt(docstring const &, docstring const &, int default_but, int, + docstring const &, docstring const &) { return default_but; } +}; + + +static ProgressInterface * progress_instance = 0; + +void ProgressInterface::setInstance(ProgressInterface* p) { - p->closeReadChannel(QProcess::StandardOutput); - p->closeReadChannel(QProcess::StandardError); - p->close(); - delete p; + progress_instance = p; } -// Reuse of instance -int Systemcall::startscript(Starttype how, string const & what) +ProgressInterface * ProgressInterface::instance() { + if (!progress_instance) { + static ProgressDummy dummy; + return &dummy; + } + return progress_instance; +} + + + + +// Reuse of instance #ifndef USE_QPROCESS - string command = what; +int Systemcall::startscript(Starttype how, string const & what, + string const & path, string const & lpath, + bool /*process_events*/) +{ + string command = + to_filesystem8bit(from_utf8(latexEnvCmdPrefix(path, lpath))) + + commandPrep(what); if (how == DontWait) { switch (os::shell()) { @@ -54,44 +118,539 @@ int Systemcall::startscript(Starttype how, string const & what) command = "start /min " + command; break; } - } + } else if (os::shell() == os::CMD_EXE) + command = subst(command, "cmd /d /c ", ""); return ::system(command.c_str()); +} + #else - QString cmd = QString::fromLocal8Bit(what.c_str()); - QProcess * process = new QProcess; - process->start(cmd); - if (!process->waitForStarted(3000)) { - LYXERR0("Qprocess " << cmd << " did not start!"); - LYXERR0("error " << process->error()); - LYXERR0("state " << process->state()); - LYXERR0("status " << process->exitStatus()); + +namespace { + +/* + * This is a parser that (mostly) mimics the behavior of a posix shell as + * regards quoting, but its output is tailored for being processed by QProcess. + * Note that shell metacharacters are not parsed. + * + * The escape character is the backslash. + * A backslash that is not quoted preserves the literal value of the following + * character, with the exception of a double-quote '"'. If a double-quote + * follows a backslash, it will be replaced by three consecutive double-quotes + * (this is how the QProcess parser recognizes a '"' as a simple character + * instead of a quoting character). Thus, for example: + * \\ -> \ + * \a -> a + * \" -> """ + * + * Single-quotes. + * Characters enclosed in single-quotes ('') have their literal value preserved. + * A single-quote cannot occur within single-quotes. Indeed, a backslash cannot + * be used to escape a single-quote in a single-quoted string. In other words, + * anything enclosed in single-quotes is passed as is, but the single-quotes + * themselves are eliminated. Thus, for example: + * '\' -> \ + * '\\' -> \\ + * '\a' -> \a + * 'a\"b' -> a\"b + * + * Double-quotes. + * Characters enclosed in double-quotes ("") have their literal value preserved, + * with the exception of the backslash. The backslash retains its special + * meaning as an escape character only when followed by a double-quote. + * Contrarily to the behavior of a posix shell, the double-quotes themselves + * are *not* eliminated. Thus, for example: + * "\\" -> "\\" + * "\a" -> "\a" + * "a\"b" -> "a"""b" + */ +string const parsecmd(string const & incmd, string & infile, string & outfile, + string & errfile) +{ + bool in_single_quote = false; + bool in_double_quote = false; + bool escaped = false; + string const python_call = "python -tt"; + vector outcmd(4); + size_t start = 0; + + if (prefixIs(incmd, python_call)) { + outcmd[0] = os::python(); + start = python_call.length(); + } + + for (size_t i = start, o = 0; i < incmd.length(); ++i) { + char c = incmd[i]; + if (c == '\'') { + if (in_double_quote || escaped) { + if (in_double_quote && escaped) + outcmd[o] += '\\'; + outcmd[o] += c; + } else + in_single_quote = !in_single_quote; + escaped = false; + continue; + } + if (in_single_quote) { + outcmd[o] += c; + continue; + } + if (c == '"') { + if (escaped) { + // Don't triple double-quotes for redirection + // files as these won't be parsed by QProcess + outcmd[o] += string(o ? "\"" : "\"\"\""); + escaped = false; + } else { + outcmd[o] += c; + in_double_quote = !in_double_quote; + } + } else if (c == '\\' && !escaped) { + escaped = !escaped; + } else if (c == '>' && !(in_double_quote || escaped)) { + if (suffixIs(outcmd[o], " 2")) { + outcmd[o] = rtrim(outcmd[o], "2"); + o = 2; + } else { + if (suffixIs(outcmd[o], " 1")) + outcmd[o] = rtrim(outcmd[o], "1"); + o = 1; + } + } else if (c == '<' && !(in_double_quote || escaped)) { + o = 3; + } else { + if (escaped && in_double_quote) + outcmd[o] += '\\'; + outcmd[o] += c; + escaped = false; + } + } + infile = trim(outcmd[3], " \""); + outfile = trim(outcmd[1], " \""); + errfile = trim(outcmd[2], " \""); + return trim(outcmd[0]); +} + +} // namespace anon + + + +int Systemcall::startscript(Starttype how, string const & what, + string const & path, string const & lpath, + bool process_events) +{ + string const what_ss = commandPrep(what); + if (verbose) + lyxerr << "\nRunning: " << what_ss << endl; + else + LYXERR(Debug::INFO,"Running: " << what_ss); + + string infile; + string outfile; + string errfile; + QString const cmd = QString::fromLocal8Bit( + parsecmd(what_ss, infile, outfile, errfile).c_str()); + + SystemcallPrivate d(infile, outfile, errfile); + +#ifdef Q_OS_WIN32 + // QProcess::startDetached cannot provide environment variables. When the + // environment variables are set using the latexEnvCmdPrefix and the process + // is started with QProcess::startDetached, a console window is shown every + // time a viewer is started. To avoid this, we fall back on Windows to the + // original implementation that creates a QProcess object. + d.startProcess(cmd, path, lpath, false); + if (!d.waitWhile(SystemcallPrivate::Starting, process_events, -1)) { + LYXERR0("Systemcall: '" << cmd << "' did not start!"); + LYXERR0("error " << d.errorMessage()); return 10; } - if (how == DontWait) + if (how == DontWait) { + d.releaseProcess(); + return 0; + } +#else + d.startProcess(cmd, path, lpath, how == DontWait); + if (how == DontWait && d.state == SystemcallPrivate::Running) return 0; - if (!process->waitForFinished(180000)) { - LYXERR0("Qprocess " << cmd << " did not finished!"); - LYXERR0("error " << process->error()); - LYXERR0("state " << process->state()); - LYXERR0("status " << process->exitStatus()); + if (d.state == SystemcallPrivate::Error + || !d.waitWhile(SystemcallPrivate::Starting, process_events, -1)) { + LYXERR0("Systemcall: '" << cmd << "' did not start!"); + LYXERR0("error " << d.errorMessage()); + return 10; + } +#endif + + if (!d.waitWhile(SystemcallPrivate::Running, process_events, + os::timeout_min() * 60 * 1000)) { + LYXERR0("Systemcall: '" << cmd << "' did not finish!"); + LYXERR0("error " << d.errorMessage()); + LYXERR0("status " << d.exitStatusMessage()); return 20; } - int const exit_code = process->exitCode(); + + int const exit_code = d.exitCode(); if (exit_code) { - LYXERR0("Qprocess " << cmd << " finished!"); - LYXERR0("exitCode " << process->exitCode()); - LYXERR0("error " << process->error()); - LYXERR0("state " << process->state()); - LYXERR0("status " << process->exitStatus()); - } - cout << fromqstr(QString::fromLocal8Bit(process->readAllStandardOutput().data())) << endl; - cerr << fromqstr(QString::fromLocal8Bit(process->readAllStandardError().data())) << endl; - killProcess(process); + LYXERR0("Systemcall: '" << cmd << "' finished with exit code " << exit_code); + } + return exit_code; +} + + +SystemcallPrivate::SystemcallPrivate(std::string const & sf, std::string const & of, + std::string const & ef) + : state(Error), process_(new QProcess), out_index_(0), err_index_(0), + in_file_(sf), out_file_(of), err_file_(ef), process_events_(false) +{ + if (!in_file_.empty()) + process_->setStandardInputFile(QString::fromLocal8Bit(in_file_.c_str())); + if (!out_file_.empty()) { + if (out_file_[0] == '&') { + if (subst(out_file_, " ", "") == "&2" + && err_file_[0] != '&') { + out_file_ = err_file_; + process_->setProcessChannelMode( + QProcess::MergedChannels); + } else { + if (err_file_[0] == '&') { + // Leave alone things such as + // "1>&2 2>&1". Should not be harmful, + // but let's give anyway a warning. + LYXERR0("Unsupported stdout/stderr redirect."); + err_file_.erase(); + } else { + LYXERR0("Ambiguous stdout redirect: " + << out_file_); + } + out_file_ = os::nulldev(); + } + } + // Check whether we have to set the output file. + if (out_file_ != os::nulldev()) { + process_->setStandardOutputFile(QString::fromLocal8Bit( + out_file_.c_str())); + } + } + if (!err_file_.empty()) { + if (err_file_[0] == '&') { + if (subst(err_file_, " ", "") == "&1" + && out_file_[0] != '&') { + process_->setProcessChannelMode( + QProcess::MergedChannels); + } else { + LYXERR0("Ambiguous stderr redirect: " + << err_file_); + } + // In MergedChannels mode stderr goes to stdout. + err_file_ = os::nulldev(); + } + // Check whether we have to set the error file. + if (err_file_ != os::nulldev()) { + process_->setStandardErrorFile(QString::fromLocal8Bit( + err_file_.c_str())); + } + } + + connect(process_, SIGNAL(readyReadStandardOutput()), SLOT(stdOut())); + connect(process_, SIGNAL(readyReadStandardError()), SLOT(stdErr())); +#if QT_VERSION >= 0x050600 + connect(process_, SIGNAL(errorOccurred(QProcess::ProcessError)), SLOT(processError(QProcess::ProcessError))); +#else + connect(process_, SIGNAL(error(QProcess::ProcessError)), SLOT(processError(QProcess::ProcessError))); #endif + connect(process_, SIGNAL(started()), this, SLOT(processStarted())); + connect(process_, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(processFinished(int, QProcess::ExitStatus))); +} + + +void SystemcallPrivate::startProcess(QString const & cmd, string const & path, + string const & lpath, bool detached) +{ + cmd_ = cmd; + if (detached) { + state = SystemcallPrivate::Running; + if (!QProcess::startDetached(toqstr(latexEnvCmdPrefix(path, lpath)) + cmd_)) { + state = SystemcallPrivate::Error; + return; + } + QProcess* released = releaseProcess(); + delete released; + } else if (process_) { + state = SystemcallPrivate::Starting; + process_->start(toqstr(latexEnvCmdPrefix(path, lpath)) + cmd_); + } +} + + +void SystemcallPrivate::processEvents() +{ + if (process_events_) { + QCoreApplication::processEvents(/*QEventLoop::ExcludeUserInputEvents*/); + } +} + + +void SystemcallPrivate::waitAndProcessEvents() +{ + Sleep::millisec(100); + processEvents(); +} + + +namespace { + +bool queryStopCommand(QString const & cmd) +{ + docstring text = bformat(_( + "The command\n%1$s\nhas not yet completed.\n\n" + "Do you want to stop it?"), qstring_to_ucs4(cmd)); + return ProgressInterface::instance()->prompt(_("Stop command?"), text, + 1, 1, _("&Stop it"), _("Let it &run")) == 0; +} + +} + + +bool SystemcallPrivate::waitWhile(State waitwhile, bool process_events, int timeout) +{ + if (!process_) + return false; + + bool timedout = false; + process_events_ = process_events; + + // Block GUI while waiting, + // relay on QProcess' wait functions + if (!process_events_) { + if (waitwhile == Starting) + return process_->waitForStarted(timeout); + if (waitwhile == Running) { + int bump = 2; + while (!timedout) { + if (process_->waitForFinished(timeout)) + return true; + bool stop = queryStopCommand(cmd_); + // The command may have finished in the meantime + if (process_->state() == QProcess::NotRunning) + return true; + if (stop) { + timedout = true; + process_->kill(); + } else { + timeout *= bump; + bump = 3; + } + } + } + return false; + } + + // process events while waiting, no timeout + if (timeout == -1) { + while (state == waitwhile && state != Error) { + waitAndProcessEvents(); + } + return state != Error; + } + + // process events while waiting with timeout + QTime timer; + timer.start(); + while (state == waitwhile && state != Error && !timedout) { + waitAndProcessEvents(); + if (timer.elapsed() > timeout) { + bool stop = queryStopCommand(cmd_); + // The command may have finished in the meantime + if (process_->state() == QProcess::NotRunning) + break; + if (stop) { + timedout = true; + process_->kill(); + } else + timeout *= 3; + } + } + return (state != Error) && !timedout; +} + + +SystemcallPrivate::~SystemcallPrivate() +{ + if (out_index_) { + out_data_[out_index_] = '\0'; + out_index_ = 0; + cout << out_data_; + } + cout.flush(); + if (err_index_) { + err_data_[err_index_] = '\0'; + err_index_ = 0; + cerr << err_data_; + } + cerr.flush(); + + killProcess(); } + +void SystemcallPrivate::stdOut() +{ + if (process_) { + char c; + process_->setReadChannel(QProcess::StandardOutput); + while (process_->getChar(&c)) { + out_data_[out_index_++] = c; + if (c == '\n' || out_index_ + 1 == buffer_size_) { + out_data_[out_index_] = '\0'; + out_index_ = 0; + ProgressInterface::instance()->appendMessage(QString::fromLocal8Bit(out_data_)); + cout << out_data_; + } + } + } +} + + +void SystemcallPrivate::stdErr() +{ + if (process_) { + char c; + process_->setReadChannel(QProcess::StandardError); + while (process_->getChar(&c)) { + err_data_[err_index_++] = c; + if (c == '\n' || err_index_ + 1 == buffer_size_) { + err_data_[err_index_] = '\0'; + err_index_ = 0; + ProgressInterface::instance()->appendError(QString::fromLocal8Bit(err_data_)); + cerr << err_data_; + } + } + } +} + + +void SystemcallPrivate::processStarted() +{ + if (state != Running) { + state = Running; + ProgressInterface::instance()->processStarted(cmd_); + } +} + + +void SystemcallPrivate::processFinished(int, QProcess::ExitStatus) +{ + if (state != Finished) { + state = Finished; + ProgressInterface::instance()->processFinished(cmd_); + } +} + + +void SystemcallPrivate::processError(QProcess::ProcessError) +{ + state = Error; + ProgressInterface::instance()->appendError(errorMessage()); +} + + +QString SystemcallPrivate::errorMessage() const +{ + if (!process_) + return "No QProcess available"; + + QString message; + switch (process_->error()) { + case QProcess::FailedToStart: + message = "The process failed to start. Either the invoked program is missing, " + "or you may have insufficient permissions to invoke the program."; + break; + case QProcess::Crashed: + message = "The process crashed some time after starting successfully."; + break; + case QProcess::Timedout: + message = "The process timed out. It might be restarted automatically."; + break; + case QProcess::WriteError: + message = "An error occurred when attempting to write to the process-> For example, " + "the process may not be running, or it may have closed its input channel."; + break; + case QProcess::ReadError: + message = "An error occurred when attempting to read from the process-> For example, " + "the process may not be running."; + break; + case QProcess::UnknownError: + default: + message = "An unknown error occurred."; + break; + } + return message; +} + + +QString SystemcallPrivate::exitStatusMessage() const +{ + if (!process_) + return "No QProcess available"; + + QString message; + switch (process_->exitStatus()) { + case QProcess::NormalExit: + message = "The process exited normally."; + break; + case QProcess::CrashExit: + message = "The process crashed."; + break; + default: + message = "Unknown exit state."; + break; + } + return message; +} + + +int SystemcallPrivate::exitCode() +{ + // From Qt's documentation, in regards to QProcess::exitCode(), + // "This value is not valid unless exitStatus() returns NormalExit" + if (!process_ || process_->exitStatus() != QProcess::NormalExit) + return -1; + + return process_->exitCode(); +} + + +QProcess* SystemcallPrivate::releaseProcess() +{ + QProcess* released = process_; + process_ = 0; + return released; +} + + +void SystemcallPrivate::killProcess() +{ + killProcess(process_); +} + + +void SystemcallPrivate::killProcess(QProcess * p) +{ + if (p) { + p->disconnect(); + p->closeReadChannel(QProcess::StandardOutput); + p->closeReadChannel(QProcess::StandardError); + p->close(); + delete p; + } +} + + + +#include "moc_SystemcallPrivate.cpp" +#endif + } // namespace support } // namespace lyx