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 }