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

0001 /*
0002     SPDX-FileCopyrightText: 2010 Miha Čančula <miha.cancula@gmail.com>
0003     SPDX-FileCopyrightText: 2017-2023 by Alexander Semke (alexander.semke@web.de)
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include <random>
0009 #include "octavesession.h"
0010 #include "octaveexpression.h"
0011 #include "octavecompletionobject.h"
0012 #include "octavesyntaxhelpobject.h"
0013 #include "octavehighlighter.h"
0014 #include "result.h"
0015 #include "textresult.h"
0016 #include <backend.h>
0017 
0018 #include "settings.h"
0019 
0020 #include <KProcess>
0021 #include <KDirWatch>
0022 #include <KLocalizedString>
0023 #include <KMessageBox>
0024 
0025 #include <QTimer>
0026 #include <QFile>
0027 #include <QDir>
0028 #include <QStringRef>
0029 
0030 #ifndef Q_OS_WIN
0031 #include <signal.h>
0032 #endif
0033 
0034 #include "octavevariablemodel.h"
0035 
0036 const QRegularExpression OctaveSession::PROMPT_UNCHANGEABLE_COMMAND = QRegularExpression(QStringLiteral("^(?:,|;)+$"));
0037 
0038 OctaveSession::OctaveSession(Cantor::Backend* backend) : Session(backend),
0039 m_prompt(QStringLiteral("CANTOR_OCTAVE_BACKEND_PROMPT:([0-9]+)> ")),
0040 m_subprompt(QStringLiteral("CANTOR_OCTAVE_BACKEND_SUBPROMPT:([0-9]+)> "))
0041 {
0042     setVariableModel(new OctaveVariableModel(this));
0043 }
0044 
0045 OctaveSession::~OctaveSession()
0046 {
0047     if (m_process)
0048     {
0049         m_process->kill();
0050         m_process->deleteLater();
0051         m_process = nullptr;
0052     }
0053 }
0054 
0055 void OctaveSession::login()
0056 {
0057     qDebug() << "login";
0058     if (m_process)
0059         return;
0060 
0061     emit loginStarted();
0062 
0063     m_process = new QProcess(this);
0064     QStringList args;
0065     args << QLatin1String("--silent");
0066     args << QLatin1String("--interactive");
0067     args << QLatin1String("--persist");
0068 
0069     // Setting prompt and subprompt
0070     args << QLatin1String("--eval");
0071     args << QLatin1String("PS1('CANTOR_OCTAVE_BACKEND_PROMPT:\\#> ');");
0072     args << QLatin1String("--eval");
0073     args << QLatin1String("PS2('CANTOR_OCTAVE_BACKEND_SUBPROMPT:\\#> ');");
0074 
0075     // Add the cantor script directory to octave script search path
0076     const QStringList& scriptDirs = locateAllCantorFiles(QLatin1String("octavebackend"), QStandardPaths::LocateDirectory);
0077     if (scriptDirs.isEmpty())
0078         qCritical() << "Octave script directory not found, needed for integrated plots";
0079     else
0080     {
0081         for (const QString& dir : scriptDirs)
0082             args << QLatin1String("--eval") << QString::fromLatin1("addpath \"%1\";").arg(dir);
0083     }
0084 
0085     // Do not show extra text in help commands
0086     args << QLatin1String("--eval");
0087     args << QLatin1String("suppress_verbose_help_message(1);");
0088 
0089     // check whether we can write to the temp folder for integrated plot files
0090     checkWritableTempFolder();
0091 
0092     m_process->start(OctaveSettings::path().toLocalFile(), args);
0093     qDebug() << "starting " << m_process->program();
0094     bool rc = m_process->waitForStarted();
0095     qDebug() << "octave process started " << rc;
0096 
0097     connect(m_process, &QProcess::readyReadStandardOutput, this, &OctaveSession::readOutput);
0098     connect(m_process, &QProcess::readyReadStandardError, this, &OctaveSession::readError);
0099     connect(m_process, &QProcess::errorOccurred, this, &OctaveSession::processError);
0100 
0101     std::random_device rd;
0102     std::mt19937 mt(rd());
0103     std::uniform_int_distribution<int> rand_dist(0, 999999999);
0104     m_plotFilePrefixPath =
0105         QDir::tempPath()
0106         + QLatin1String("/cantor_octave_")
0107         + QString::number(m_process->processId())
0108         + QLatin1String("_")
0109         + QString::number(rand_dist(mt))
0110         + QLatin1String("_");
0111 
0112     if(!OctaveSettings::self()->autorunScripts().isEmpty()){
0113         QString autorunScripts = OctaveSettings::self()->autorunScripts().join(QLatin1String("\n"));
0114         evaluateExpression(autorunScripts, OctaveExpression::DeleteOnFinish, true);
0115         updateVariables();
0116     }
0117 
0118     if (!m_worksheetPath.isEmpty())
0119     {
0120         static const QString mfilenameTemplate = QLatin1String(
0121             "function retval = mfilename(arg_mem = \"\")\n"
0122                 "type_info=typeinfo(arg_mem);\n"
0123                 "if (strcmp(type_info, \"string\"))\n"
0124                     "if (strcmp(arg_mem, \"fullpath\"))\n"
0125                         "retval = \"%1\";\n"
0126                     "elseif (strcmp(arg_mem, \"fullpathext\"))\n"
0127                         "retval = \"%2\";\n"
0128                     "else\n"
0129                         "retval = \"script\";\n"
0130                     "endif\n"
0131                 "else\n"
0132                     "error(\"wrong type argument '%s'\", type_info)\n"
0133                 "endif\n"
0134             "endfunction"
0135         );
0136         const QString& worksheetDirPath = QFileInfo(m_worksheetPath).absoluteDir().absolutePath();
0137         const QString& worksheetPathWithoutExtension = m_worksheetPath.mid(0, m_worksheetPath.lastIndexOf(QLatin1Char('.')));
0138 
0139         evaluateExpression(QLatin1String("cd ")+worksheetDirPath, OctaveExpression::DeleteOnFinish, true);
0140         evaluateExpression(mfilenameTemplate.arg(worksheetPathWithoutExtension, m_worksheetPath), OctaveExpression::DeleteOnFinish, true);
0141     }
0142 
0143     changeStatus(Cantor::Session::Done);
0144     emit loginDone();
0145     qDebug()<<"login done";
0146 }
0147 
0148 void OctaveSession::setWorksheetPath(const QString& path)
0149 {
0150     m_worksheetPath = path;
0151 }
0152 
0153 void OctaveSession::logout()
0154 {
0155     qDebug()<<"logout";
0156 
0157     if(!m_process)
0158         return;
0159 
0160     disconnect(m_process, nullptr, this, nullptr);
0161 
0162     if(status() == Cantor::Session::Running)
0163         interrupt();
0164 
0165     m_process->write("exit\n");
0166     qDebug()<<"send exit command to octave";
0167 
0168     if(!m_process->waitForFinished(1000))
0169     {
0170         m_process->kill();
0171         qDebug()<<"octave still running, process kill enforced";
0172     }
0173     m_process->deleteLater();
0174     m_process = nullptr;
0175 
0176     if (!m_plotFilePrefixPath.isEmpty())
0177     {
0178         int i = 0;
0179         const QString& extension = OctaveExpression::plotExtensions[OctaveSettings::inlinePlotFormat()];
0180         QString filename = m_plotFilePrefixPath + QString::number(i) + QLatin1String(".") + extension;
0181         while (QFile::exists(filename))
0182         {
0183             QFile::remove(filename);
0184             i++;
0185             filename = m_plotFilePrefixPath + QString::number(i) + QLatin1String(".") + extension;
0186         }
0187     }
0188 
0189     expressionQueue().clear();
0190 
0191     m_output.clear();
0192     m_previousPromptNumber = 1;
0193 
0194     Session::logout();
0195 
0196     qDebug()<<"logout done";
0197 }
0198 
0199 void OctaveSession::interrupt()
0200 {
0201     qDebug() << expressionQueue().size();
0202     if(!expressionQueue().isEmpty())
0203     {
0204         qDebug()<<"interrupting " << expressionQueue().first()->command();
0205         if(m_process && m_process->state() != QProcess::NotRunning)
0206         {
0207 #ifndef Q_OS_WIN
0208             const int pid = m_process->processId();
0209             kill(pid, SIGINT);
0210 #else
0211             ; //TODO: interrupt the process on windows
0212 #endif
0213         }
0214 
0215         for (auto* expression : expressionQueue())
0216             expression->setStatus(Cantor::Expression::Interrupted);
0217         expressionQueue().clear();
0218 
0219         // Cleanup inner state and call octave prompt printing
0220         // If we move this code for interruption to Session, we need add function for
0221         // cleaning before setting Done status
0222         m_output.clear();
0223         m_process->write("\n");
0224 
0225         qDebug()<<"done interrupting";
0226     }
0227 
0228     changeStatus(Cantor::Session::Done);
0229 }
0230 
0231 void OctaveSession::processError()
0232 {
0233     qDebug() << "processError";
0234     emit error(m_process->errorString());
0235 }
0236 
0237 Cantor::Expression* OctaveSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behavior, bool internal)
0238 {
0239     qDebug()<<"################################## EXPRESSION START ###############################################";
0240     qDebug() << "evaluating: " << cmd;
0241     auto* expression = new OctaveExpression(this, internal);
0242     expression->setCommand(cmd);
0243     expression->setFinishingBehavior(behavior);
0244     expression->evaluate();
0245 
0246     return expression;
0247 }
0248 
0249 void OctaveSession::runFirstExpression()
0250 {
0251     qDebug()<< "OctaveSession::runFirstExpression()";
0252     auto* expression = expressionQueue().first();
0253     connect(expression, &Cantor::Expression::statusChanged, this, &Session::currentExpressionStatusChanged);
0254 
0255     const auto& command = expression->internalCommand();
0256     if (isDoNothingCommand(command))
0257         expression->setStatus(Cantor::Expression::Done);
0258     else
0259     {
0260         expression->setStatus(Cantor::Expression::Computing);
0261         qDebug()<<"writing " << command.toLocal8Bit();
0262         m_process->write(command.toLocal8Bit());
0263     }
0264 }
0265 
0266 void OctaveSession::readError()
0267 {
0268     QString error = QString::fromLocal8Bit(m_process->readAllStandardError());
0269     if (!expressionQueue().isEmpty() && !error.isEmpty())
0270     {
0271         auto* const exp = expressionQueue().first();
0272         if (m_syntaxError)
0273         {
0274             m_syntaxError = false;
0275             exp->parseError(i18n("Syntax Error"));
0276         }
0277         else
0278             exp->parseError(error);
0279 
0280         m_output.clear();
0281     }
0282 }
0283 
0284 void OctaveSession::readOutput()
0285 {
0286     while (m_process->bytesAvailable() > 0)
0287     {
0288         QString line = QString::fromLocal8Bit(m_process->readLine());
0289 
0290         qDebug()<<"start parsing " << "  " << line;
0291         QRegularExpressionMatch match = m_prompt.match(line);
0292         if (match.hasMatch())
0293         {
0294             const int promptNumber = match.captured(1).toInt();
0295             // Add all text before prompt, if exists
0296             m_output += QStringRef(&line, 0, match.capturedStart(0)).toString();
0297             if (!expressionQueue().isEmpty())
0298             {
0299                 const QString& command = expressionQueue().first()->command();
0300                 if (m_previousPromptNumber + 1 == promptNumber || isSpecialOctaveCommand(command))
0301                 {
0302                     if (!expressionQueue().isEmpty())
0303                     {
0304                         readError();
0305                         expressionQueue().first()->parseOutput(m_output);
0306                     }
0307                 }
0308                 else
0309                 {
0310                     // Error command don't increase octave prompt number (usually, but not always)
0311                     readError();
0312                 }
0313             }
0314             m_previousPromptNumber = promptNumber;
0315             m_output.clear();
0316         }
0317         else if ((match = m_subprompt.match(line)).hasMatch()
0318                  && match.captured(1).toInt() == m_previousPromptNumber)
0319         {
0320             // User don't write finished octave statement (for example, write 'a = [1,2, ' only), so
0321             // octave print subprompt and waits input finish.
0322             m_syntaxError = true;
0323             qDebug() << "subprompt catch";
0324             m_process->write(")]'\"\n"); // force exit from subprompt
0325             m_output.clear();
0326         }
0327         else
0328             m_output += line;
0329     }
0330 }
0331 
0332 Cantor::CompletionObject* OctaveSession::completionFor(const QString& cmd, int index)
0333 {
0334     return new OctaveCompletionObject(cmd, index, this);
0335 }
0336 
0337 Cantor::SyntaxHelpObject* OctaveSession::syntaxHelpFor(const QString& cmd)
0338 {
0339     return new OctaveSyntaxHelpObject(cmd, this);
0340 }
0341 
0342 QSyntaxHighlighter* OctaveSession::syntaxHighlighter(QObject* parent)
0343 {
0344     return new OctaveHighlighter(parent, this);
0345 }
0346 
0347 bool OctaveSession::isDoNothingCommand(const QString& command)
0348 {
0349     return PROMPT_UNCHANGEABLE_COMMAND.match(command).hasMatch()
0350            || command.isEmpty() || command == QLatin1String("\n");
0351 }
0352 
0353 bool OctaveSession::isSpecialOctaveCommand(const QString& command)
0354 {
0355     return command.contains(QLatin1String("completion_matches"));
0356 }
0357 
0358 bool OctaveSession::isIntegratedPlotsEnabled() const
0359 {
0360     return OctaveSettings::integratePlots() && m_writableTempFolder;
0361 }
0362 
0363 QString OctaveSession::plotFilePrefixPath() const
0364 {
0365     return m_plotFilePrefixPath;
0366 }
0367 
0368 /*!
0369  * check whether we can write to the temp folder for integrated plot files.
0370  */
0371 void OctaveSession::checkWritableTempFolder() {
0372     QString filename = QDir::tempPath() + QLatin1String("/cantor_octave_plot_integration_test.txt");
0373     QFile::remove(filename); // Remove previous file, if present
0374     int test_number = rand() % 1000;
0375 
0376     QStringList args;
0377     args << QLatin1String("--no-init-file");
0378     args << QLatin1String("--no-gui");
0379     args << QLatin1String("--eval");
0380     args << QString::fromLatin1("file_id = fopen('%1', 'w'); fdisp(file_id, %2); fclose(file_id);").arg(filename).arg(test_number);
0381 
0382     QString errorMsg;
0383     m_writableTempFolder = Cantor::Backend::testProgramWritable(
0384         OctaveSettings::path().toLocalFile(),
0385         args,
0386         filename,
0387         QString::number(test_number),
0388         &errorMsg
0389     );
0390 
0391     if (!m_writableTempFolder)
0392     {
0393         KMessageBox::error(nullptr,
0394             i18n("Plot integration test failed.")+
0395             QLatin1String("\n\n")+
0396             errorMsg+
0397             QLatin1String("\n\n")+
0398             i18n("The integration of plots will be disabled."),
0399             i18n("Cantor")
0400         );
0401     }
0402 }