File indexing completed on 2024-05-26 11:22:06

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009-2012 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2017-2022 Alexander Semke (alexander.semke@web.de)
0005 */
0006 
0007 #include "maximasession.h"
0008 #include "maximaexpression.h"
0009 #include "maximacompletionobject.h"
0010 #include "maximasyntaxhelpobject.h"
0011 #include "maximahighlighter.h"
0012 #include "maximavariablemodel.h"
0013 #include "result.h"
0014 #include "settings.h"
0015 
0016 #include <QDebug>
0017 #include <QTimer>
0018 #include <QStandardPaths>
0019 
0020 #include <KMessageBox>
0021 #include <KLocalizedString>
0022 
0023 #ifndef Q_OS_WIN
0024 #include <signal.h>
0025 #endif
0026 
0027 
0028 //NOTE: the \\s in the expressions is needed, because Maxima seems to sometimes insert newlines/spaces between the letters
0029 //maybe this is caused by some behaviour if the Prompt is split into multiple "readStdout" calls
0030 //the Expressions are encapsulated in () to allow capturing for the text
0031 const QRegularExpression MaximaSession::MaximaOutputPrompt =
0032             QRegularExpression(QStringLiteral("(\\(\\s*%\\s*o\\s*[0-9\\s]*\\))")); //Text, maxima outputs, before any output
0033 const QRegularExpression MaximaSession::MaximaInputPrompt =
0034             QRegularExpression(QStringLiteral("(\\(\\s*%\\s*i\\s*[0-9\\s]*\\))"));
0035 
0036 
0037 MaximaSession::MaximaSession( Cantor::Backend* backend ) : Session(backend)
0038 {
0039     setVariableModel(new MaximaVariableModel(this));
0040 }
0041 
0042 void MaximaSession::login()
0043 {
0044     qDebug()<<"login";
0045 
0046     if (m_process)
0047         return; //TODO: why do we call login() again?!?
0048 
0049     emit loginStarted();
0050     QStringList arguments;
0051     arguments << QLatin1String("--quiet"); //Suppress Maxima start-up message
0052     const QString initFile = locateCantorFile(QLatin1String("maximabackend/cantor-initmaxima.lisp"));
0053     arguments << QLatin1String("--init-lisp=") + initFile; //Set the name of the Lisp initialization file
0054 
0055     m_process = new QProcess(this);
0056     m_process->start(MaximaSettings::self()->path().toLocalFile(), arguments);
0057     m_process->waitForStarted();
0058 
0059     // Wait until first maxima prompt
0060     QString input;
0061     while (!input.contains(QLatin1String("</cantor-prompt>")))
0062     {
0063         m_process->waitForReadyRead();
0064         input += QString::fromLatin1(m_process->readAllStandardOutput());
0065         qDebug() << input;
0066     }
0067 
0068     connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(restartMaxima()));
0069     connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readStdOut()));
0070     connect(m_process, SIGNAL(readyReadStandardError()), this, SLOT(readStdErr()));
0071     connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(reportProcessError(QProcess::ProcessError)));
0072 
0073     //enable latex typesetting if needed
0074     const QString& val = QLatin1String((isTypesettingEnabled() ? "t":"nil"));
0075     evaluateExpression(QString::fromLatin1(":lisp(setf $display2d %1)").arg(val), Cantor::Expression::DeleteOnFinish, true);
0076 
0077     //auto-run scripts
0078     if(!MaximaSettings::self()->autorunScripts().isEmpty()){
0079         QString autorunScripts = MaximaSettings::self()->autorunScripts().join(QLatin1String(";"));
0080         autorunScripts.append(QLatin1String(";kill(labels)")); // Reset labels after running autorun scripts
0081         evaluateExpression(autorunScripts, MaximaExpression::DeleteOnFinish, true);
0082         updateVariables();
0083     }
0084 
0085     changeStatus(Session::Done);
0086     emit loginDone();
0087     qDebug()<<"login done";
0088 }
0089 
0090 void MaximaSession::logout()
0091 {
0092     qDebug()<<"logout";
0093 
0094     if(!m_process)
0095         return;
0096 
0097     disconnect(m_process, nullptr, this, nullptr);
0098 
0099     if (status() == Cantor::Session::Running)
0100         interrupt();
0101 
0102     write(QLatin1String("quit();\n"));
0103     qDebug()<<"waiting for maxima to finish";
0104 
0105     if(!m_process->waitForFinished(1000))
0106     {
0107         m_process->kill();
0108         qDebug()<<"maxima still running, process kill enforced";
0109     }
0110     m_process->deleteLater();
0111     m_process = nullptr;
0112 
0113     Session::logout();
0114 
0115     qDebug()<<"logout done";
0116 }
0117 
0118 MaximaSession::Mode MaximaSession::mode() const {
0119     return m_mode;
0120 }
0121 
0122 void MaximaSession::setMode(MaximaSession::Mode mode)
0123 {
0124     m_mode = mode;
0125 }
0126 
0127 Cantor::Expression* MaximaSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal)
0128 {
0129     qDebug() << "evaluating: " << cmd;
0130     auto* expr = new MaximaExpression(this, internal);
0131     expr->setFinishingBehavior(behave);
0132     expr->setCommand(cmd);
0133     expr->evaluate();
0134 
0135     return expr;
0136 }
0137 
0138 void MaximaSession::readStdErr()
0139 {
0140    qDebug()<<"reading stdErr";
0141    if (!m_process)
0142        return;
0143    QString out = QString::fromLocal8Bit(m_process->readAllStandardError());
0144 
0145    if(expressionQueue().size()>0)
0146    {
0147        auto* expr = expressionQueue().first();
0148        expr->parseError(out);
0149    }
0150 }
0151 
0152 void MaximaSession::readStdOut()
0153 {
0154     QString out = QString::fromLocal8Bit(m_process->readAllStandardOutput());
0155     m_cache += out;
0156 
0157     //collect the multi-line output until Maxima has finished the calculation and returns a new promt
0158     if ( !out.contains(QLatin1String("</cantor-prompt>")) )
0159         return;
0160 
0161     if(expressionQueue().isEmpty())
0162     {
0163         //queue is empty, interrupt was called, nothing to do here
0164         qDebug()<<m_cache;
0165         m_cache.clear();
0166         return;
0167     }
0168 
0169     auto* expr = expressionQueue().first();
0170     if (!expr)
0171         return; //should never happen
0172 
0173     qDebug()<<"output: " << m_cache;
0174     expr->parseOutput(m_cache);
0175     m_cache.clear();
0176 }
0177 
0178 void MaximaSession::reportProcessError(QProcess::ProcessError e)
0179 {
0180     qDebug()<<"process error"<<e;
0181     if(e==QProcess::FailedToStart)
0182     {
0183         changeStatus(Cantor::Session::Done);
0184         emit error(i18n("Failed to start Maxima"));
0185     }
0186 }
0187 
0188 void MaximaSession::runFirstExpression()
0189 {
0190     qDebug()<<"running next expression";
0191     if (!m_process)
0192         return;
0193 
0194     if(!expressionQueue().isEmpty())
0195     {
0196         auto* expr = expressionQueue().first();
0197         const auto& command = expr->internalCommand();
0198         connect(expr, &Cantor::Expression::statusChanged, this, &Session::currentExpressionStatusChanged);
0199 
0200         if(command.isEmpty())
0201         {
0202             qDebug()<<"empty command";
0203             static_cast<MaximaExpression*>(expr)->forceDone();
0204         }
0205         else
0206         {
0207             expr->setStatus(Cantor::Expression::Computing);
0208             m_cache.clear();
0209             write(command + QLatin1Char('\n'));
0210         }
0211     }
0212 }
0213 
0214 void MaximaSession::interrupt()
0215 {
0216     if(!expressionQueue().isEmpty())
0217     {
0218         qDebug()<<"interrupting " << expressionQueue().first()->command();
0219         if(m_process && m_process->state() != QProcess::NotRunning)
0220         {
0221 #ifndef Q_OS_WIN
0222             const int pid = m_process->processId();
0223             kill(pid, SIGINT);
0224 #else
0225             ; //TODO: interrupt the process on windows
0226 #endif
0227         }
0228        for (auto* expression : expressionQueue())
0229             expression->setStatus(Cantor::Expression::Interrupted);
0230 
0231         expressionQueue().clear();
0232 
0233         qDebug()<<"done interrupting";
0234     }
0235 
0236     changeStatus(Cantor::Session::Done);
0237     m_cache.clear();
0238 }
0239 
0240 void MaximaSession::sendInputToProcess(const QString& input)
0241 {
0242     write(input);
0243 }
0244 
0245 void MaximaSession::restartMaxima()
0246 {
0247     qDebug()<<"restarting maxima cooldown: " << m_justRestarted;
0248 
0249     if(!m_justRestarted)
0250     {
0251         emit error(i18n("Maxima crashed. restarting..."));
0252         //remove the command that caused maxima to crash (to avoid infinite loops)
0253         if(!expressionQueue().isEmpty())
0254             expressionQueue().removeFirst();
0255 
0256         m_justRestarted=true;
0257         QTimer::singleShot(1000, this, SLOT(restartsCooledDown()));
0258 
0259         disconnect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(restartMaxima()));
0260         login();
0261     }else
0262     {
0263         if(!expressionQueue().isEmpty())
0264             expressionQueue().removeFirst();
0265         KMessageBox::error(nullptr, i18n("Maxima crashed twice within a short time. Stopping to try starting"), i18n("Error - Cantor"));
0266     }
0267 }
0268 
0269 void MaximaSession::restartsCooledDown()
0270 {
0271     qDebug()<<"maxima restart cooldown";
0272     m_justRestarted=false;
0273 }
0274 
0275 void MaximaSession::setTypesettingEnabled(bool enable)
0276 {
0277     if (m_process)
0278     {
0279         //we use the lisp command to set the variable, as those commands
0280         //don't mess with the labels and history
0281         const QString& val = QLatin1String((enable ? "t":"nil"));
0282         evaluateExpression(QString::fromLatin1(":lisp(setf $display2d %1)").arg(val), Cantor::Expression::DeleteOnFinish, true);
0283     }
0284 
0285     Cantor::Session::setTypesettingEnabled(enable);
0286 }
0287 
0288 Cantor::CompletionObject* MaximaSession::completionFor(const QString& command, int index)
0289 {
0290     return new MaximaCompletionObject(command, index, this);
0291 }
0292 
0293 Cantor::SyntaxHelpObject* MaximaSession::syntaxHelpFor(const QString& command)
0294 {
0295     return new MaximaSyntaxHelpObject(command, this);
0296 }
0297 
0298 QSyntaxHighlighter* MaximaSession::syntaxHighlighter(QObject* parent)
0299 {
0300     return new MaximaHighlighter(parent, this);
0301 }
0302 
0303 void MaximaSession::write(const QString& exp) {
0304     qDebug()<<"################################## EXPRESSION START ###############################################";
0305     qDebug()<<"sending expression to maxima process: " << exp;
0306     m_process->write(exp.toUtf8());
0307 }