X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;f=src%2Fsupport%2FSystemcall.cpp;h=7c4eaabc979490ae61930b290425582e83e262e5;hb=b198a36a363bb6a084407d476942d68ef5fb5e86;hp=e4fa5b255992354620cd786a5eb76a25293fec4e;hpb=0eedbdee03dd697a5f6bccd0438fd3947da740b0;p=lyx.git diff --git a/src/support/Systemcall.cpp b/src/support/Systemcall.cpp index e4fa5b2559..7c4eaabc97 100644 --- a/src/support/Systemcall.cpp +++ b/src/support/Systemcall.cpp @@ -14,8 +14,8 @@ #include #include "support/debug.h" -#include "support/environment.h" #include "support/filetools.h" +#include "support/gettext.h" #include "support/lstrings.h" #include "support/qstring_helpers.h" #include "support/Systemcall.h" @@ -23,13 +23,13 @@ #include "support/os.h" #include "support/ProgressInterface.h" -#include "LyXRC.h" +#include "LyX.h" #include #include #include -#include +#include #include #include #include @@ -39,7 +39,7 @@ struct Sleep : QThread { - static void millisec(unsigned long ms) + static void millisec(unsigned long ms) { QThread::usleep(ms * 1000); } @@ -54,29 +54,31 @@ namespace lyx { namespace support { -class ProgressDummy : public ProgressInterface +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 &) {} - void information(QString const &, QString const &) {} + void processStarted(QString const &) override {} + void processFinished(QString const &) override {} + void appendMessage(QString const &) override {} + void appendError(QString const &) override {} + void clearMessages() override {} + void lyxerrFlush() override {} + + void lyxerrConnect() override {} + void lyxerrDisconnect() override {} + + void warning(QString const &, QString const &) override {} + void toggleWarning(QString const &, QString const &, QString const &) override {} + void error(QString const &, QString const &, QString const &) override {} + void information(QString const &, QString const &) override {} + int prompt(docstring const &, docstring const &, int default_but, int, + docstring const &, docstring const &) override { return default_but; } }; -static ProgressInterface* progress_instance = 0; +static ProgressInterface * progress_instance = nullptr; void ProgressInterface::setInstance(ProgressInterface* p) { @@ -84,7 +86,7 @@ void ProgressInterface::setInstance(ProgressInterface* p) } -ProgressInterface* ProgressInterface::instance() +ProgressInterface * ProgressInterface::instance() { if (!progress_instance) { static ProgressDummy dummy; @@ -99,26 +101,12 @@ ProgressInterface* ProgressInterface::instance() // Reuse of instance #ifndef USE_QPROCESS int Systemcall::startscript(Starttype how, string const & what, - std::string const & path, bool /*process_events*/) + string const & path, string const & lpath, + bool /*process_events*/) { - string command; - string const texinputs = os::latex_path_list( - replaceCurdirPath(path, lyxrc.texinputs_prefix)); - string const sep = string(1, os::path_separator(os::TEXENGINE)); - string const env = getEnv("TEXINPUTS"); - - switch (os::shell()) { - case os::UNIX: - command = path.empty() || lyxrc.texinputs_prefix.empty() ? what - : "env TEXINPUTS='." + sep + texinputs - + sep + env + "' " + what; - break; - case os::CMD_EXE: - command = path.empty() || lyxrc.texinputs_prefix.empty() ? what - : "set TEXINPUTS=." + sep + texinputs - + sep + env + " & " + what; - break; - } + string command = + to_filesystem8bit(from_utf8(latexEnvCmdPrefix(path, lpath))) + + commandPrep(what); if (how == DontWait) { switch (os::shell()) { @@ -129,7 +117,8 @@ 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()); } @@ -139,8 +128,9 @@ int Systemcall::startscript(Starttype how, string const & what, namespace { /* - * This is a parser that (mostly) mimics the behavior of a posix shell but - * its output is tailored for being processed by QProcess. + * 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 @@ -173,85 +163,142 @@ namespace { * "\a" -> "\a" * "a\"b" -> "a"""b" */ -string const parsecmd(string const & inputcmd, string & outfile) +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 cmd; + string const python_call = os::python(); + vector outcmd(4); + size_t start = 0; - for (size_t i = 0; i < inputcmd.length(); ++i) { - char c = inputcmd[i]; + 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) - cmd += '\\'; - cmd += c; + outcmd[o] += '\\'; + outcmd[o] += c; } else in_single_quote = !in_single_quote; escaped = false; continue; } if (in_single_quote) { - cmd += c; + outcmd[o] += c; continue; } if (c == '"') { if (escaped) { - cmd += "\"\"\""; + // Don't triple double-quotes for redirection + // files as these won't be parsed by QProcess + outcmd[o] += string(o ? "\"" : "\"\"\""); escaped = false; } else { - cmd += c; + outcmd[o] += c; in_double_quote = !in_double_quote; } } else if (c == '\\' && !escaped) { - escaped = !escaped; + escaped = true; } else if (c == '>' && !(in_double_quote || escaped)) { - outfile = trim(inputcmd.substr(i + 1), " \""); - return trim(cmd); + 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; +#if defined (USE_MACOSX_PACKAGING) + } else if (o == 0 && i > 4 && c == ' ' && !(in_double_quote || escaped)) { + // if a macOS app is detected with an additional argument + // use open command as prefix to get it work + const size_t apos = outcmd[o].rfind(".app"); + const size_t len = outcmd[o].length(); + const bool quoted = outcmd[o].at(len - 1) == '"' && outcmd[o].at(0) == '"'; + const string & ocmd = "open -a "; + if (apos != string::npos && + (apos == (len - 4) || (apos == (len - 5) && quoted)) && + !prefixIs(trim(outcmd[o]), ocmd)) { + outcmd[o] = ocmd + outcmd[o]; + } + outcmd[o] += c; +#endif } else { if (escaped && in_double_quote) - cmd += '\\'; - cmd += c; + outcmd[o] += '\\'; + outcmd[o] += c; escaped = false; } } - outfile.erase(); - return cmd; + infile = trim(outcmd[3], " \""); + outfile = trim(outcmd[1], " \""); + errfile = trim(outcmd[2], " \""); + return trim(outcmd[0]); } -} // namespace anon +} // namespace - -int Systemcall::startscript(Starttype how, string const & what, - string const & path, bool process_events) +void Systemcall::killscript() { - string outfile; - QString cmd = toqstr(parsecmd(what, outfile)); - - SystemcallPrivate d(outfile); + SystemcallPrivate::kill_script = true; +} - d.startProcess(cmd, path); - if (!d.waitWhile(SystemcallPrivate::Starting, process_events, -1)) { - LYXERR0("Systemcall: '" << cmd << "' did not start!"); - LYXERR0("error " << d.errorMessage()); - return 10; - } +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); - if (how == DontWait) { - QProcess* released = d.releaseProcess(); - (void) released; // TODO who deletes it? - return 0; + string infile; + string outfile; + string errfile; + QString const cmd = QString::fromLocal8Bit( + parsecmd(what_ss, infile, outfile, errfile).c_str()); + + SystemcallPrivate d(infile, outfile, errfile); + bool do_events = process_events || how == WaitLoop; + + d.startProcess(cmd, path, lpath, how == DontWait); + if (how == DontWait && d.state == SystemcallPrivate::Running) + return OK; + + if (d.state == SystemcallPrivate::Error + || !d.waitWhile(SystemcallPrivate::Starting, do_events, -1)) { + if (d.state == SystemcallPrivate::Error) { + LYXERR0("Systemcall: '" << cmd << "' did not start!"); + LYXERR0("error " << d.errorMessage()); + return NOSTART; + } else if (d.state == SystemcallPrivate::Killed) { + LYXERR0("Killed: " << cmd); + return KILLED; + } } - if (!d.waitWhile(SystemcallPrivate::Running, process_events, - os::timeout_min() * 60 * 1000)) { + if (!d.waitWhile(SystemcallPrivate::Running, do_events, + os::timeout_ms())) { + if (d.state == SystemcallPrivate::Killed) { + LYXERR0("Killed: " << cmd); + return KILLED; + } LYXERR0("Systemcall: '" << cmd << "' did not finish!"); LYXERR0("error " << d.errorMessage()); LYXERR0("status " << d.exitStatusMessage()); - return 20; + return TIMEOUT; } int const exit_code = d.exitCode(); @@ -263,70 +310,169 @@ int Systemcall::startscript(Starttype how, string const & what, } -SystemcallPrivate::SystemcallPrivate(const std::string& of) : - process_(new QProcess), - out_index_(0), - err_index_(0), - out_file_(of), - texinputs_(getEnv("TEXINPUTS")), - process_events_(false) +bool SystemcallPrivate::kill_script = false; + + +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()) { - // Check whether we have to simply throw away the output. - if (out_file_ != os::nulldev()) - process_->setStandardOutputFile(toqstr(out_file_)); + 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) +void SystemcallPrivate::startProcess(QString const & cmd, string const & path, + string const & lpath, bool detached) { cmd_ = cmd; - if (process_) { - if (!path.empty() && !lyxrc.texinputs_prefix.empty()) { - string const texinputs = os::latex_path_list( - replaceCurdirPath(path, lyxrc.texinputs_prefix)); - string const sep = string(1, - os::path_separator(os::TEXENGINE)); - string const prefix = "." + sep + texinputs + sep; - if (prefixIs(texinputs_, prefix)) - texinputs_.clear(); - else - setEnv("TEXINPUTS", prefix + texinputs_); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + // FIXME pass command and arguments separated in the first place + /* The versions of startDetached() and start() that accept a + * QStringList object exist since Qt4, but it is only in Qt 5.15 + * that splitCommand() was introduced and the plain versions of + * start/startDetached() have been deprecated. + * The cleanest solution would be to have parsecmd() produce a + * QStringList for arguments, instead of transforming the string + * into something that the QProcess splitter accepts. + * + * Another reason for doing that is that the Qt parser ignores + * empty "" arguments, which are needed in some instances (see + * e.g. the work around for modules in Converter:convert. See + * QTBUG-80640 for a discussion. + */ + QStringList arguments = QProcess::splitCommand(toqstr(latexEnvCmdPrefix(path, lpath)) + cmd_); + QString command = (arguments.empty()) ? QString() : arguments.first(); + if (arguments.size() == 1) + arguments.clear(); + else if (!arguments.empty()) + arguments.removeFirst(); +#endif + if (detached) { + state = SystemcallPrivate::Running; +#ifdef Q_OS_WIN32 + // Avoid opening a console window when a viewer is started + if (in_file_.empty()) + process_->setStandardInputFile(QProcess::nullDevice()); + if (out_file_.empty()) + process_->setStandardOutputFile(QProcess::nullDevice()); + if (err_file_.empty()) + process_->setStandardErrorFile(QProcess::nullDevice()); +#endif +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + if (!QProcess::startDetached(command, arguments)) { +#else + if (!QProcess::startDetached(toqstr(latexEnvCmdPrefix(path, lpath)) + cmd_)) { +#endif + state = SystemcallPrivate::Error; + return; } + QProcess* released = releaseProcess(); + delete released; + } else if (process_) { state = SystemcallPrivate::Starting; - process_->start(cmd_); +#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) + process_->start(command, arguments); +#else + process_->start(toqstr(latexEnvCmdPrefix(path, lpath)) + cmd_); +#endif } } -void SystemcallPrivate::processEvents() +bool SystemcallPrivate::waitAndCheck() { - if(process_events_) { - QCoreApplication::processEvents(/*QEventLoop::ExcludeUserInputEvents*/); + Sleep::millisec(100); + if (kill_script) { + // is there a better place to reset this? + process_->kill(); + state = Killed; + kill_script = false; + LYXERR0("Export Canceled!!"); + return false; } + QCoreApplication::processEvents(/*QEventLoop::ExcludeUserInputEvents*/); + return true; } -void SystemcallPrivate::waitAndProcessEvents() +namespace { + +bool queryStopCommand(QString const & cmd) { - Sleep::millisec(100); - processEvents(); + 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; } +} // namespace + 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, @@ -334,34 +480,63 @@ bool SystemcallPrivate::waitWhile(State waitwhile, bool process_events, int time if (!process_events_) { if (waitwhile == Starting) return process_->waitForStarted(timeout); - if (waitwhile == Running) - return process_->waitForFinished(timeout); + if (waitwhile == Running) { + int bump = 2; + while (!timedout) { + if (process_->waitForFinished(timeout)) + return true; + bool const 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(); + // check for cancellation of background process + if (!waitAndCheck()) + return false; } return state != Error; - } + } - // process events while waiting whith timeout - QTime timer; + // process events while waiting with timeout + QElapsedTimer timer; timer.start(); - while (state == waitwhile && state != Error && timer.elapsed() < timeout) { - waitAndProcessEvents(); + while (state == waitwhile && state != Error && !timedout) { + // check for cancellation of background process + if (!waitAndCheck()) + return false; + + if (timer.elapsed() > timeout) { + bool const 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) && (timer.elapsed() < timeout); + return (state != Error) && !timedout; } SystemcallPrivate::~SystemcallPrivate() { - if (!texinputs_.empty()) - setEnv("TEXINPUTS", texinputs_); - if (out_index_) { out_data_[out_index_] = '\0'; out_index_ = 0; @@ -440,7 +615,7 @@ void SystemcallPrivate::processError(QProcess::ProcessError) } -QString SystemcallPrivate::errorMessage() const +QString SystemcallPrivate::errorMessage() const { if (!process_) return "No QProcess available"; @@ -467,7 +642,7 @@ QString SystemcallPrivate::errorMessage() const break; case QProcess::UnknownError: default: - message = "An unknown error occured."; + message = "An unknown error occurred."; break; } return message; @@ -497,7 +672,9 @@ QString SystemcallPrivate::exitStatusMessage() const int SystemcallPrivate::exitCode() { - if (!process_) + // 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(); @@ -507,7 +684,7 @@ int SystemcallPrivate::exitCode() QProcess* SystemcallPrivate::releaseProcess() { QProcess* released = process_; - process_ = 0; + process_ = nullptr; return released; }