File indexing completed on 2024-04-28 11:20:42

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2023 Alexander Semke <alexander.semke@web.de>
0005 */
0006 
0007 #include "sagesession.h"
0008 #include "sageexpression.h"
0009 #include "sagecompletionobject.h"
0010 #include "sagehighlighter.h"
0011 #include "settings.h"
0012 
0013 #include <QDebug>
0014 #include <QRegularExpression>
0015 
0016 #include <KLocalizedString>
0017 #include <KMessageBox>
0018 
0019 #ifndef Q_OS_WIN
0020 #include <signal.h>
0021 #endif
0022 
0023 const QByteArray SageSession::SagePrompt = "sage: "; //Text, sage outputs after each command
0024 const QByteArray SageSession::SageAlternativePrompt = "....: "; //Text, sage outputs when it expects further input
0025 
0026 //some commands that are run after login
0027 static QByteArray initCmd = "import os\n"\
0028                            "os.environ['PAGER'] = 'cat'                     \n "\
0029                            "sage.misc.pager.EMBEDDED_MODE = True           \n "\
0030                            "sage.misc.viewer.BROWSER=''                    \n "\
0031                            "sage.plot.plot3d.base.SHOW_DEFAULTS['viewer'] = 'tachyon' \n"\
0032                            "sage.misc.latex.EMBEDDED_MODE = True           \n "\
0033                            "%colors nocolor                                \n "\
0034                            "try: \n "\
0035                            "    SAGE_TMP = sage.misc.temporary_file.TMP_DIR_FILENAME_BASE.name \n "\
0036                            "except AttributeError: \n "\
0037                            "    SAGE_TMP = sage.misc.misc.SAGE_TMP \n "\
0038                            "print('%s %s' % ('____TMP_DIR____', SAGE_TMP))\n";
0039 
0040 static QByteArray newInitCmd =
0041     "__CANTOR_IPYTHON_SHELL__=get_ipython()   \n "\
0042     "__CANTOR_IPYTHON_SHELL__.autoindent=False\n ";
0043 
0044 static QByteArray endOfInitMarker = "print('____END_OF_INIT____')\n ";
0045 
0046 
0047 SageSession::VersionInfo::VersionInfo(int major, int minor)
0048 {
0049     m_major = major;
0050     m_minor = minor;
0051 }
0052 
0053 int SageSession::VersionInfo::majorVersion() const
0054 {
0055     return m_major;
0056 }
0057 
0058 int SageSession::VersionInfo::minorVersion() const
0059 {
0060     return m_minor;
0061 }
0062 
0063 bool SageSession::VersionInfo::operator==(VersionInfo other) const
0064 {
0065     return m_major == other.m_major && m_minor==other.m_minor;
0066 }
0067 
0068 bool SageSession::VersionInfo::operator<(VersionInfo other) const
0069 {
0070     return (m_major != -1 && other.m_major == -1) ||
0071         ( ((m_major !=- 1 && other.m_major != -1) || (m_major == other.m_major && m_major == -1) )
0072         && ( m_major < other.m_major || (m_major == other.m_major && m_minor < other.m_minor) ) );
0073 }
0074 
0075 bool SageSession::VersionInfo::operator<=(VersionInfo other) const
0076 {
0077     return (*this < other)||(*this == other);
0078 }
0079 
0080 bool SageSession::VersionInfo::operator>(SageSession::VersionInfo other) const
0081 {
0082     return !( (*this <= other ));
0083 }
0084 
0085 bool SageSession::VersionInfo::operator>=(SageSession::VersionInfo other) const
0086 {
0087     return !( *this < other);
0088 }
0089 
0090 SageSession::SageSession(Cantor::Backend* backend) : Session(backend)
0091 {
0092     connect(&m_dirWatch, &KDirWatch::created, this, &SageSession::fileCreated);
0093 }
0094 
0095 SageSession::~SageSession()
0096 {
0097     if (m_process)
0098     {
0099         m_process->kill();
0100         m_process->deleteLater();
0101         m_process = nullptr;
0102     }
0103 }
0104 
0105 void SageSession::login()
0106 {
0107     qDebug()<<"login";
0108     if (m_process)
0109         return;
0110     emit loginStarted();
0111 
0112     updateSageVersion();
0113 
0114     QStringList arguments;
0115     arguments << QLatin1String("-q"); // suppress the banner
0116     arguments << QLatin1String("--simple-prompt"); // suppress the colorizing of the output
0117 
0118     m_process = new QProcess(this);
0119     m_process->start(SageSettings::self()->path().toLocalFile(), arguments);
0120     m_process->waitForStarted();
0121 
0122     connect(m_process, &QProcess::readyRead, this, &SageSession::readStdOut);
0123     connect(m_process, &QProcess::readyReadStandardOutput, this, &SageSession::readStdOut);
0124     connect(m_process, &QProcess::readyReadStandardError, this, &SageSession::readStdErr);
0125     connect(m_process, &QProcess::errorOccurred, this, &SageSession::reportProcessError);
0126     connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus)));
0127 
0128     // initialize the settings for embeded plots
0129     // TODO: right now the settings are evaluated during the login only and the user needs to re-login
0130     // or to restart the application to get the changes applied. A better logic would be to recognize
0131     // a plot command and to apply the changes "on the fly" as in maximasession for example or
0132     // to apply the changes when the user has modified the settings only. Both options are not
0133     // available for Sage yet and should be implemented later.
0134     if (SageSettings::self()->integratePlots())
0135     {
0136         // deactivate external viewers
0137         initCmd += "sage.misc.viewer.viewer.png_viewer('false')\n";
0138         initCmd += "sage.misc.viewer.viewer.pdf_viewer('false')\n";
0139     }
0140     else
0141     {
0142         initCmd += "sage.misc.viewer.viewer.png_viewer('true')\n";
0143         initCmd += "sage.misc.viewer.viewer.pdf_viewer('true')\n";
0144     }
0145 
0146     if (SageSettings::inlinePlotFormat() == 0) // PDF
0147         initCmd += "sage.repl.rich_output.get_display_manager().preferences.graphics = 'vector' \n";
0148     else // PNG
0149         initCmd += "sage.repl.rich_output.get_display_manager().preferences.graphics = 'raster' \n";
0150 
0151     // matplotlib's figure accepts the sizes in inches
0152     double w = SageSettings::plotWidth() / 2.54;
0153     double h = SageSettings::plotHeight() / 2.54;
0154     initCmd += "import matplotlib.pyplot as plt; plt.rcParams['figure.figsize'] = [" + QString::number(w).toLatin1() + ", " + QString::number(h).toLatin1() + "]\n";
0155 
0156     m_process->write(initCmd);
0157 
0158     //save the path to the worksheet as variable "__file__"
0159     //this variable is usually set by the "os" package when running a script
0160     //but when it is run in an interpreter (like sage server) it is not set
0161     if (!m_worksheetPath.isEmpty())
0162     {
0163         const QString cmd = QLatin1String("__file__ = '%1'");
0164         evaluateExpression(cmd.arg(m_worksheetPath), Cantor::Expression::DeleteOnFinish, true);
0165     }
0166 
0167     //enable latex typesetting if needed
0168     const QString cmd = QLatin1String("__cantor_enable_typesetting(%1)");
0169     evaluateExpression(cmd.arg(isTypesettingEnabled() ? QLatin1String("true"):QLatin1String("false")),
0170                        Cantor::Expression::DeleteOnFinish);
0171 
0172     //auto-run scripts
0173     if(!SageSettings::self()->autorunScripts().isEmpty()){
0174         QString autorunScripts = SageSettings::self()->autorunScripts().join(QLatin1String("\n"));
0175         evaluateExpression(autorunScripts, SageExpression::DeleteOnFinish, true);
0176     }
0177 
0178     changeStatus(Session::Done);
0179     emit loginDone();
0180 }
0181 
0182 void SageSession::logout()
0183 {
0184     qDebug()<<"logout";
0185 
0186     if (!m_process)
0187         return;
0188 
0189     if(status() == Cantor::Session::Running)
0190         interrupt();
0191 
0192     disconnect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished(int,QProcess::ExitStatus)));
0193 
0194     m_process->write("exit\n");
0195 
0196     if(!m_process->waitForFinished(1000))
0197         m_process->kill();
0198     m_process->deleteLater();
0199     m_process = nullptr;
0200 
0201     m_isInitialized = false;
0202     m_waitingForPrompt = false;
0203     m_haveSentInitCmd = false;
0204 
0205     Session::logout();
0206 }
0207 
0208 Cantor::Expression* SageSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal)
0209 {
0210     qDebug() << "evaluating: " << cmd;
0211     auto* expr = new SageExpression(this, internal);
0212     expr->setFinishingBehavior(behave);
0213     expr->setCommand(cmd);
0214     expr->evaluate();
0215 
0216     return expr;
0217 }
0218 
0219 void SageSession::readStdOut()
0220 {
0221     QString out = QString::fromUtf8(m_process->readAllStandardOutput());
0222     if (out.isEmpty())
0223         return;
0224 
0225     qDebug()<<"out: " << out;
0226     m_outputCache += out;
0227 
0228     if ( m_outputCache.contains( QLatin1String("___TMP_DIR___") ) )
0229     {
0230         int index = m_outputCache.indexOf(QLatin1String("___TMP_DIR___") )+14;
0231         int endIndex = m_outputCache.indexOf(QLatin1String("\n"), index);
0232 
0233         if(endIndex == -1)
0234             m_tmpPath = m_outputCache.mid( index ).trimmed();
0235         else
0236             m_tmpPath = m_outputCache.mid( index, endIndex-index ).trimmed();
0237 
0238         qDebug()<<"tmp path: "<<m_tmpPath;
0239 
0240         m_dirWatch.addDir(m_tmpPath, KDirWatch::WatchFiles);
0241     }
0242 
0243     if(!m_isInitialized)
0244     {
0245         // enforce the minimal version Sage 9.2 (released Oct 24, 2020).
0246         // this is the version supporting the --simple prompt CLI option that simplifies the initialization of sage,
0247         // s.a. the discussions in
0248         // https://github.com/sagemath/sage/issues/25363
0249         // https://bugs.kde.org/show_bug.cgi?id=408176
0250         if(updateSageVersion())
0251         {
0252             if(m_sageVersion <= SageSession::VersionInfo(9, 2))
0253             {
0254                 const QString& message = i18n("Sage version %1.%2 is unsupported. Please update your installation to the versions 9.2 or higher.",
0255                                              m_sageVersion.majorVersion(), m_sageVersion.minorVersion());
0256                 KMessageBox::error(nullptr, message, i18n("Unsupported Version"));
0257                 interrupt();
0258                 logout();
0259             }
0260             else
0261             {
0262                 qDebug()<<"using the current set of commands";
0263 
0264                 if(!m_haveSentInitCmd)
0265                 {
0266                     m_process->write(newInitCmd);
0267                     defineCustomFunctions();
0268                     m_process->write(endOfInitMarker);
0269                     m_haveSentInitCmd=true;
0270                 }
0271             }
0272         }
0273         else
0274         {
0275             const QString& message = i18n("Failed to determine the version of Sage. Please check your installation and the output of 'sage -v'.");
0276             KMessageBox::error(nullptr, message, i18n("Unsupported Version"));
0277             interrupt();
0278             logout();
0279         }
0280     }
0281 
0282 
0283     int indexOfEOI = m_outputCache.indexOf(QLatin1String("____END_OF_INIT____"));
0284     if(indexOfEOI != -1 && m_outputCache.indexOf(QLatin1String(SagePrompt), indexOfEOI) != -1)
0285     {
0286         qDebug() << "initialized";
0287         //out.remove("____END_OF_INIT____");
0288         //out.remove(SagePrompt);
0289         m_isInitialized = true;
0290         m_waitingForPrompt = false;
0291         runFirstExpression();
0292         m_outputCache.clear();
0293     }
0294 
0295     //If we are waiting for another prompt, drop every output
0296     //until a prompt is found
0297     if(m_isInitialized && m_waitingForPrompt)
0298     {
0299         qDebug() << "waiting for prompt";
0300         if(m_outputCache.contains(QLatin1String(SagePrompt)))
0301             m_waitingForPrompt = false;
0302 
0303         m_outputCache.clear();
0304         return;
0305     }
0306 
0307     if(m_isInitialized)
0308     {
0309         if (!expressionQueue().isEmpty())
0310         {
0311             auto* expr = expressionQueue().first();
0312             expr->parseOutput(m_outputCache);
0313         }
0314         m_outputCache.clear();
0315     }
0316 }
0317 
0318 void SageSession::readStdErr()
0319 {
0320     qDebug()<<"reading stdErr";
0321     QString out = QLatin1String(m_process->readAllStandardError());
0322     if (!expressionQueue().isEmpty())
0323     {
0324         auto* expr = expressionQueue().first();
0325         expr->parseError(out);
0326     }
0327 }
0328 
0329 void SageSession::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
0330 {
0331     Q_UNUSED(exitCode);
0332     if(exitStatus == QProcess::CrashExit)
0333     {
0334         if(!expressionQueue().isEmpty())
0335         {
0336             static_cast<SageExpression*>(expressionQueue().last())
0337                 ->onProcessError(i18n("The Sage process crashed while evaluating this expression"));
0338         }else
0339         {
0340             //We don't have an actual command. it crashed for some other reason, just show a plain error message box
0341             KMessageBox::error(nullptr, i18n("The Sage process crashed"), i18n("Cantor"));
0342         }
0343     }else
0344     {
0345         if(!expressionQueue().isEmpty())
0346         {
0347             static_cast<SageExpression*>(expressionQueue().last())
0348                 ->onProcessError(i18n("The Sage process exited while evaluating this expression"));
0349         }else
0350         {
0351             //We don't have an actual command. it crashed for some other reason, just show a plain error message box
0352             KMessageBox::error(nullptr, i18n("The Sage process exited"), i18n("Cantor"));
0353         }
0354     }
0355 }
0356 
0357 void SageSession::reportProcessError(QProcess::ProcessError e)
0358 {
0359     if(e == QProcess::FailedToStart)
0360     {
0361         changeStatus(Cantor::Session::Done);
0362         emit error(i18n("Failed to start Sage"));
0363     }
0364 }
0365 
0366 void SageSession::runFirstExpression()
0367 {
0368     if(!expressionQueue().isEmpty())
0369     {
0370         auto* expr = expressionQueue().first();
0371 
0372         if (m_isInitialized)
0373         {
0374             connect(expr, &Cantor::Expression::statusChanged, this, &Session::currentExpressionStatusChanged);
0375 
0376             QString command = expr->command();
0377             if(command.endsWith(QLatin1Char('?')) && !command.endsWith(QLatin1String("??")))
0378                 command=QLatin1String("help(")+command.left(command.size()-1)+QLatin1Char(')');
0379             if(command.startsWith(QLatin1Char('?')))
0380                 command=QLatin1String("help(")+command.mid(1)+QLatin1Char(')');
0381             command.append(QLatin1String("\n\n"));
0382 
0383             qDebug()<<"writing "<<command<<" to the process";
0384             expr->setStatus(Cantor::Expression::Computing);
0385             m_process->write(command.toUtf8());
0386         }
0387         else if (expressionQueue().size() == 1)
0388             // If queue contains one expression, it means, what we run this expression immediately (drop setting queued status)
0389             // TODO: Sage login is slow, so, maybe better mark this expression as queued for a login time
0390             expr->setStatus(Cantor::Expression::Queued);
0391     }
0392 }
0393 
0394 void SageSession::interrupt()
0395 {
0396     if(!expressionQueue().isEmpty())
0397     {
0398         qDebug()<<"interrupting " << expressionQueue().first()->command();
0399         if(m_process && m_process->state() != QProcess::NotRunning)
0400         {
0401 #ifndef Q_OS_WIN
0402             const int pid = m_process->processId();
0403             kill(pid, SIGINT);
0404 #else
0405             ; //TODO: interrupt the process on windows
0406 #endif
0407         }
0408 
0409         for (auto* expression : expressionQueue())
0410             expression->setStatus(Cantor::Expression::Interrupted);
0411 
0412         expressionQueue().clear();
0413         m_outputCache.clear();
0414 
0415         qDebug()<<"done interrupting";
0416     }
0417 
0418     changeStatus(Cantor::Session::Done);
0419 }
0420 
0421 void SageSession::sendInputToProcess(const QString& input)
0422 {
0423     m_process->write(input.toUtf8());
0424 }
0425 
0426 void SageSession::fileCreated( const QString& path )
0427 {
0428     if (!SageSettings::self()->integratePlots())
0429         return;
0430 
0431     qDebug()<<"got a file " << path;
0432     if (!expressionQueue().isEmpty())
0433     {
0434         auto* expr = static_cast<SageExpression*>(expressionQueue().first());
0435         if (expr)
0436            expr->addFileResult( path );
0437     }
0438 }
0439 
0440 void SageSession::setTypesettingEnabled(bool enable)
0441 {
0442     if (m_process)
0443     {
0444         //tell the sage server to enable/disable pretty_print
0445         const QString cmd = QLatin1String("__cantor_enable_typesetting(%1)");
0446         evaluateExpression(cmd.arg(enable ? QLatin1String("true"):QLatin1String("false")), Cantor::Expression::DeleteOnFinish);
0447     }
0448 
0449     Cantor::Session::setTypesettingEnabled(enable);
0450 }
0451 
0452 void SageSession::setWorksheetPath(const QString& path)
0453 {
0454     m_worksheetPath = path;
0455 }
0456 
0457 Cantor::CompletionObject* SageSession::completionFor(const QString& command, int index)
0458 {
0459     return new SageCompletionObject(command, index, this);
0460 }
0461 
0462 QSyntaxHighlighter* SageSession::syntaxHighlighter(QObject* parent)
0463 {
0464     return new SageHighlighter(parent);
0465 }
0466 
0467 SageSession::VersionInfo SageSession::sageVersion()
0468 {
0469     return m_sageVersion;
0470 }
0471 
0472 void SageSession::defineCustomFunctions()
0473 {
0474     //typesetting
0475     QString cmd = QLatin1String("def __cantor_enable_typesetting(enable):\n"\
0476                                 "\t if(enable==true):\n "\
0477                                 "\t \t %display typeset \n"\
0478                                 "\t else: \n" \
0479                                 "\t \t %display simple \n\n");
0480 
0481     sendInputToProcess(cmd);
0482 }
0483 
0484 bool SageSession::updateSageVersion()
0485 {
0486     QProcess get_sage_version;
0487     get_sage_version.setProgram(SageSettings::self()->path().toLocalFile());
0488     get_sage_version.setArguments(QStringList() << QLatin1String("-v"));
0489     get_sage_version.start();
0490     if (!get_sage_version.waitForFinished(-1))
0491         return false;
0492 
0493     QString versionString = QString::fromLocal8Bit(get_sage_version.readLine());
0494     QRegularExpression versionExp(QLatin1String("(\\d+)\\.(\\d+)"));
0495     QRegularExpressionMatch version = versionExp.match(versionString);
0496     qDebug()<<"found version: " << version.capturedTexts();
0497     if(version.capturedTexts().length() == 3)
0498     {
0499         int major = version.capturedTexts().at(1).toInt();
0500         int minor = version.capturedTexts().at(2).toInt();
0501         m_sageVersion = SageSession::VersionInfo(major, minor);
0502         return true;
0503     }
0504     else
0505         return false;
0506 }