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 }