diff --git a/src/backends/python/pythonsession.cpp b/src/backends/python/pythonsession.cpp index f7d243a0..5497a5ee 100644 --- a/src/backends/python/pythonsession.cpp +++ b/src/backends/python/pythonsession.cpp @@ -1,282 +1,280 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2012 Filipe Saraiva Copyright (C) 2015 Minh Ngo */ #include #include "pythonsession.h" #include "pythonexpression.h" #include "pythonvariablemodel.h" #include "pythonhighlighter.h" #include "pythoncompletionobject.h" #include "pythonkeywords.h" #include "pythonutils.h" #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #endif const QChar recordSep(30); const QChar unitSep(31); const QChar messageEnd = 29; PythonSession::PythonSession(Cantor::Backend* backend, int pythonVersion, const QString serverName) : Session(backend) , m_process(nullptr) , serverName(serverName) , m_pythonVersion(pythonVersion) { setVariableModel(new PythonVariableModel(this)); } PythonSession::~PythonSession() { if (m_process) { disconnect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError); m_process->kill(); m_process->deleteLater(); m_process = nullptr; } } void PythonSession::login() { qDebug()<<"login"; emit loginStarted(); if (m_process) m_process->deleteLater(); m_process = new QProcess(this); m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel); m_process->start(QStandardPaths::findExecutable(serverName)); m_process->waitForStarted(); m_process->waitForReadyRead(); QTextStream stream(m_process->readAllStandardOutput()); const QString& readyStatus = QString::fromLatin1("ready"); while (m_process->state() == QProcess::Running) { const QString& rl = stream.readLine(); if (rl == readyStatus) break; } connect(m_process, &QProcess::readyReadStandardOutput, this, &PythonSession::readOutput); connect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError); sendCommand(QLatin1String("login")); QString dir; if (!worksheetPath.isEmpty()) dir = QFileInfo(worksheetPath).absoluteDir().absolutePath(); sendCommand(QLatin1String("setFilePath"), QStringList() << worksheetPath << dir); const QStringList& scripts = autorunScripts(); if(!scripts.isEmpty()){ QString autorunScripts = scripts.join(QLatin1String("\n")); evaluateExpression(autorunScripts, Cantor::Expression::DeleteOnFinish, true); variableModel()->update(); } changeStatus(Session::Done); emit loginDone(); } void PythonSession::logout() { if (!m_process) return; - sendCommand(QLatin1String("exit")); + if (m_process->exitStatus() != QProcess::CrashExit && m_process->error() != QProcess::WriteError) + sendCommand(QLatin1String("exit")); if(m_process->state() == QProcess::Running && !m_process->waitForFinished(1000)) { disconnect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError); m_process->kill(); qDebug()<<"cantor_python server still running, process kill enforced"; } m_process->deleteLater(); m_process = nullptr; qDebug()<<"logout"; Session::logout(); } void PythonSession::interrupt() { if(!expressionQueue().isEmpty()) { qDebug()<<"interrupting " << expressionQueue().first()->command(); if(m_process && m_process->state() != QProcess::NotRunning) { #ifndef Q_OS_WIN const int pid=m_process->pid(); kill(pid, SIGINT); #else ; //TODO: interrupt the process on windows #endif } for (Cantor::Expression* expression : expressionQueue()) expression->setStatus(Cantor::Expression::Interrupted); expressionQueue().clear(); m_output.clear(); qDebug()<<"done interrupting"; } changeStatus(Cantor::Session::Done); } Cantor::Expression* PythonSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal) { qDebug() << "evaluating: " << cmd; PythonExpression* expr = new PythonExpression(this, internal); changeStatus(Cantor::Session::Running); expr->setFinishingBehavior(behave); expr->setCommand(cmd); expr->evaluate(); return expr; } QSyntaxHighlighter* PythonSession::syntaxHighlighter(QObject* parent) { return new PythonHighlighter(parent, this, m_pythonVersion); } Cantor::CompletionObject* PythonSession::completionFor(const QString& command, int index) { return new PythonCompletionObject(command, index, this); } void PythonSession::runFirstExpression() { if (expressionQueue().isEmpty()) return; Cantor::Expression* expr = expressionQueue().first(); const QString command = expr->internalCommand(); qDebug() << "run first expression" << command; expr->setStatus(Cantor::Expression::Computing); if (expr->isInternal() && command.startsWith(QLatin1String("%variables "))) { const QString arg = command.section(QLatin1String(" "), 1); sendCommand(QLatin1String("model"), QStringList(arg)); } else sendCommand(QLatin1String("code"), QStringList(expr->internalCommand())); } void PythonSession::sendCommand(const QString& command, const QStringList arguments) const { qDebug() << "send command: " << command << arguments; const QString& message = command + recordSep + arguments.join(unitSep) + messageEnd; m_process->write(message.toLocal8Bit()); } void PythonSession::readOutput() { while (m_process->bytesAvailable() > 0) { const QByteArray& bytes = m_process->readAll(); if (m_pythonVersion == 3) m_output.append(QString::fromUtf8(bytes)); else if (m_pythonVersion == 2) m_output.append(QString::fromLocal8Bit(bytes)); else qCritical() << "Unsupported Python version" << m_pythonVersion; } qDebug() << "m_output: " << m_output; if (!m_output.contains(messageEnd)) return; const QStringList packages = m_output.split(messageEnd, QString::SkipEmptyParts); if (m_output.endsWith(messageEnd)) m_output.clear(); else m_output = m_output.section(messageEnd, -1); for (const QString& message: packages) { if (expressionQueue().isEmpty()) break; const QString& output = message.section(unitSep, 0, 0); const QString& error = message.section(unitSep, 1, 1); bool isError = message.section(unitSep, 2, 2).toInt(); if (isError) { if(error.isEmpty()){ static_cast(expressionQueue().first())->parseOutput(output); } else { static_cast(expressionQueue().first())->parseError(error); } } else { static_cast(expressionQueue().first())->parseWarning(error); static_cast(expressionQueue().first())->parseOutput(output); } finishFirstExpression(true); } } void PythonSession::setWorksheetPath(const QString& path) { worksheetPath = path; } void PythonSession::reportServerProcessError(QProcess::ProcessError serverError) { switch(serverError) { case QProcess::Crashed: emit error(i18n("Cantor Python server stopped working.")); break; case QProcess::FailedToStart: emit error(i18n("Failed to start Cantor python server.")); break; default: emit error(i18n("Communication with Cantor python server failed for unknown reasons.")); break; } - // Server crash, and now m_process is invalid, so remove m_process object - m_process->deleteLater(); - m_process = nullptr; - Session::logout(); + reportSessionCrash(); } diff --git a/src/lib/session.cpp b/src/lib/session.cpp index 62bd5f00..e779149e 100644 --- a/src/lib/session.cpp +++ b/src/lib/session.cpp @@ -1,233 +1,247 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2009 Alexander Rieder */ #include "session.h" using namespace Cantor; #include "backend.h" #include "defaultvariablemodel.h" #include #include +#include +#include class Cantor::SessionPrivate { public: SessionPrivate() : backend(nullptr), status(Session::Disable), typesettingEnabled(false), expressionCount(0), variableModel(nullptr), needUpdate(false) { } Backend* backend; Session::Status status; bool typesettingEnabled; int expressionCount; QList expressionQueue; DefaultVariableModel* variableModel; bool needUpdate; }; Session::Session( Backend* backend ) : QObject(backend), d(new SessionPrivate) { d->backend=backend; } Session::Session( Backend* backend, DefaultVariableModel* model) : QObject(backend), d(new SessionPrivate) { d->backend=backend; d->variableModel=model; } Session::~Session() { delete d; } void Session::logout() { if (d->status == Session::Running) interrupt(); if (d->variableModel) { d->variableModel->clearVariables(); d->variableModel->clearFunctions(); } d->expressionCount = 0; changeStatus(Status::Disable); } QList& Cantor::Session::expressionQueue() const { return d->expressionQueue; } void Session::enqueueExpression(Expression* expr) { d->expressionQueue.append(expr); //run the newly added expression immediately if it's the only one in the queue if (d->expressionQueue.size() == 1) { changeStatus(Cantor::Session::Running); runFirstExpression(); } else expr->setStatus(Cantor::Expression::Queued); } void Session::runFirstExpression() { } void Session::finishFirstExpression(bool setDoneAfterUpdate) { if (!d->expressionQueue.isEmpty()) d->needUpdate |= !d->expressionQueue.takeFirst()->isInternal(); if (d->expressionQueue.isEmpty()) if (d->variableModel && d->needUpdate) { d->variableModel->update(); d->needUpdate = false; // Some variable models could update internal lists without running expressions // So, if after update queue still empty, set status to Done // setDoneAfterUpdate used for compatibility with some backends, like R if (setDoneAfterUpdate && d->expressionQueue.isEmpty()) changeStatus(Done); } else changeStatus(Done); else runFirstExpression(); } Backend* Session::backend() { return d->backend; } Cantor::Session::Status Session::status() { return d->status; } void Session::changeStatus(Session::Status newStatus) { d->status=newStatus; emit statusChanged(newStatus); } void Session::setTypesettingEnabled(bool enable) { d->typesettingEnabled=enable; } bool Session::isTypesettingEnabled() { return d->typesettingEnabled; } void Session::setWorksheetPath(const QString& path) { Q_UNUSED(path); return; } CompletionObject* Session::completionFor(const QString& cmd, int index) { Q_UNUSED(cmd); Q_UNUSED(index); //Return 0 per default, so Backends not offering tab completions don't have //to reimplement this. This method should only be called on backends with //the Completion Capability flag return nullptr; } SyntaxHelpObject* Session::syntaxHelpFor(const QString& cmd) { Q_UNUSED(cmd); //Return 0 per default, so Backends not offering tab completions don't have //to reimplement this. This method should only be called on backends with //the SyntaxHelp Capability flag return nullptr; } QSyntaxHighlighter* Session::syntaxHighlighter(QObject* parent) { Q_UNUSED(parent); return nullptr; } DefaultVariableModel* Session::variableModel() const { - //Return deafult session model per default - //By default, variableModel is nullptr, so Backends not offering variable management don't - //have to reimplement this. This method should only be called on backends with - //VariableManagement Capability flag + //By default, there is variableModel in session, used by syntax higlighter for variable analyzing + //The model store only variable names by default. + //In backends with VariableManagement Capability flag, this model also used for Cantor variable doc panel return d->variableModel; } QAbstractItemModel* Session::variableDataModel() const { return variableModel(); } void Session::updateVariables() { if (d->variableModel) { d->variableModel->update(); d->needUpdate = false; } } void Cantor::Session::setVariableModel(Cantor::DefaultVariableModel* model) { d->variableModel = model; } int Session::nextExpressionId() { return d->expressionCount++; } QString Session::locateCantorFile(const QString& partialPath, QStandardPaths::LocateOptions options) { QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, partialPath, options); if (file.isEmpty()) file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("cantor/") + partialPath, options); return file; } QStringList Session::locateAllCantorFiles(const QString& partialPath, QStandardPaths::LocateOptions options) { QStringList files = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, partialPath, options); if (files.isEmpty()) files = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QLatin1String("cantor/") + partialPath, options); return files; } + +void Cantor::Session::reportSessionCrash(const QString& additionalInfo) +{ + // Reporting about crashing backend in session without backend has not sense + if (d->backend == nullptr) + return; + + if (additionalInfo.isEmpty()) + KMessageBox::error(nullptr, i18n("%1 process has died unexpectedly. All calculation results are lost.", d->backend->name()), i18n("Error - Cantor")); + else + KMessageBox::error(nullptr, i18n("%1 process has died unexpectedly with message \"%2\". All calculation results are lost.", d->backend->name()), i18n("Error - Cantor")); + logout(); +} diff --git a/src/lib/session.h b/src/lib/session.h index da91cbfd..581943f7 100644 --- a/src/lib/session.h +++ b/src/lib/session.h @@ -1,264 +1,273 @@ /* This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. --- Copyright (C) 2009 Alexander Rieder */ #ifndef _SESSION_H #define _SESSION_H #include #include #include "cantor_export.h" #include "expression.h" #include "defaultvariablemodel.h" class QTextEdit; class QSyntaxHighlighter; class QAbstractItemModel; /** * Namespace collecting all Classes of the Cantor Libraries */ namespace Cantor { class Backend; class SessionPrivate; class CompletionObject; class SyntaxHelpObject; class DefaultVariableModel; /** * The Session object is the main class used to interact with a Backend. * It is used to evaluate Expressions, get completions, syntax highlighting, etc. * * @author Alexander Rieder */ class CANTOR_EXPORT Session : public QObject { Q_OBJECT public: enum Status { Running, ///< the session is busy, running some expression Done, ///< the session has done all the jobs, and is now waiting for more Disable ///< the session don't login yet, or already logout }; /** * Create a new Session. This should not yet set up the complete session, * thats job of the login() function * @see login() */ explicit Session( Backend* backend); /** * Similar to Session::Session, but also specify variable model for automatically handles model's updates */ explicit Session( Backend* backend, DefaultVariableModel* model); /** * Destructor */ ~Session() override; /** * Login to the Session. In this function you should do anything needed to set up * the session, and make it ready for usage. The method should be implemented non-blocking. * Emit loginStarted() prior to connection to the actual backend in order to notify cantor_part about it. * If the logging in is completed, the loginDone() signal must be emitted */ virtual void login() = 0; /** * Log out of the Session. Destroy everything specific to a single session, e.g. * stop all the running processes etc. Also after logout session status must be Status::Disable * Default implementation does basic operations for all sessions (for example, variable model cleanup) * NOTE: restarting the session consists of first logout() and then login() */ virtual void logout(); /** * Passes the given command to the backend and returns a Pointer * to a new Expression object, which will emit the gotResult() * signal as soon as the computation is done. The result will * then be accessible by Expression::result() * @param command the command that should be run by the backend. * @param finishingBehavior the FinishingBehaviour that should be used for this command. @see Expression::FinishingBehaviour * @param internal true, if it is an internal command @see Expression::Expression(Session*, bool) * @return an Expression object, representing this command */ virtual Expression* evaluateExpression(const QString& command, Expression::FinishingBehavior finishingBehavior = Expression::FinishingBehavior::DoNotDelete, bool internal = false) = 0; /** * Append the expression to queue . * @see expressionQueue() const */ void enqueueExpression(Expression*); /** * Interrupts all the running calculations in this session * After this function expression queue must be clean */ virtual void interrupt() = 0; /** * Returns tab-completion, for this command/command-part. * The return type is a CompletionObject. The fetching * of the completions works asynchronously, you'll have to * listen to the done() Signal of the returned object * @param cmd The partial command that should be completed * @param index The index (cursor position) at which completion * was invoked. Defaults to -1, indicating the end of the string. * @return a Completion object, representing this completion * @see CompletionObject */ virtual CompletionObject* completionFor(const QString& cmd, int index = -1); /** * Returns Syntax help, for this command. * It returns a SyntaxHelpObject, that will fetch the * needed information asynchronously. You need to listen * to the done() Signal of the Object * @param cmd the command, syntax help is requested for * @return SyntaxHelpObject, representing the help request * @see SyntaxHelpObject */ virtual SyntaxHelpObject* syntaxHelpFor(const QString& cmd); /** * returns a syntax highlighter for this session * @param parent QObject the Highlighter's parent * @return QSyntaxHighlighter doing the highlighting for this Session */ virtual QSyntaxHighlighter* syntaxHighlighter(QObject* parent); /** * returns a Model to interact with the variables or nullptr, if * this backend have a variable model, which not inherit from * default variable model class (in this case @see variableDataModel()) * @return DefaultVariableModel to interact with the variables */ virtual DefaultVariableModel* variableModel() const; /** * returns QAbstractItemModel to interact with the variables */ virtual QAbstractItemModel* variableDataModel() const; /** * Enables/disables Typesetting for this session. * For this setting to make effect, the Backend must support * LaTeX typesetting (as indicated by the capabilities() flag. * @param enable true to enable, false to disable typesetting */ virtual void setTypesettingEnabled(bool enable); /** * Updates the worksheet path in the session. * This can be useful to set the path of the currently opened * Cantor project file in the backend interpreter. * Default implementation does nothing. Derived classes have * to implement the proper logic if this feature is supported. * @param path the new absolute path to the worksheet. */ virtual void setWorksheetPath(const QString& path); /** * Returns the Backend, this Session is for * @return the Backend, this Session is for */ Backend* backend(); /** * Returns the status this Session has * @return the status this Session has */ Cantor::Session::Status status(); /** * Returns whether typesetting is enabled or not * @return whether typesetting is enabled or not */ bool isTypesettingEnabled(); /** * Returns the next available Expression id * It is basically a counter, incremented for * each new Expression * @return next Expression id */ int nextExpressionId(); protected: /** * Change the status of the Session. This will cause the * stausChanged signal to be emitted * @param newStatus the new status of the session */ void changeStatus(Cantor::Session::Status newStatus); /** * Session can process one single expression at one time. * Any other expressions submitted by the user are queued first until they get processed. * The expression queue implements the FIFO mechanism. * The queud expression have the status \c Expression::Queued. */ QList& expressionQueue() const; /** * Execute first expression in expression queue. * Also, this function changes the status from Queued to Computing. * @see expressionQueue() const */ virtual void runFirstExpression(); /** * This method dequeues the expression and goes to the next expression, if the queue is not empty. * Also, this method updates the variable model, if needed. * If the queue is empty, the session status is set to Done. * @param setDoneAfterUpdate enable setting status to Done after variable update, if queue is empty */ virtual void finishFirstExpression(bool setDoneAfterUpdate = false); /** * Starts variable update immideatly, usefull for subclasses, which run internal command * which could change variables listen */ virtual void updateVariables(); /** * Setting variable model, usefull, if model constructor requires functional session */ void setVariableModel(DefaultVariableModel* model); /** * Search file for session in AppDataLocation and in GenericDataLocation */ QString locateCantorFile(const QString& partialPath, QStandardPaths::LocateOptions options = QStandardPaths::LocateFile); QStringList locateAllCantorFiles(const QString& partialPath, QStandardPaths::LocateOptions options = QStandardPaths::LocateFile); + /** + * Sometimes backend process/server could crash, stop responding, in other words, session can't + * continue to work without restart. + * This method will notify about session crashing with automatically logout + * and another actions, which needed to do in situations like that + */ + void reportSessionCrash(const QString& additionalInfo = QString()); + + Q_SIGNALS: void statusChanged(Cantor::Session::Status newStatus); void loginStarted(); void loginDone(); void error(const QString& msg); private: SessionPrivate* d; }; } #endif /* _SESSION_H */