1 /*
2     SPDX-FileCopyrightText: 2010 Miha Čančula <miha.cancula@gmail.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include <random>
8 #include "octavesession.h"
9 #include "octaveexpression.h"
10 #include "octavecompletionobject.h"
11 #include "octavesyntaxhelpobject.h"
12 #include "octavehighlighter.h"
13 #include "result.h"
14 #include "textresult.h"
15 #include <backend.h>
16 
17 #include "settings.h"
18 
19 #include <KProcess>
20 #include <KDirWatch>
21 #include <KLocalizedString>
22 #include <KMessageBox>
23 
24 #include <QTimer>
25 #include <QFile>
26 #include <QDir>
27 #include <QStringRef>
28 
29 #ifndef Q_OS_WIN
30 #include <signal.h>
31 #endif
32 
33 #include "octavevariablemodel.h"
34 
35 const QRegularExpression OctaveSession::PROMPT_UNCHANGEABLE_COMMAND = QRegularExpression(QStringLiteral("^(?:,|;)+$"));
36 
OctaveSession(Cantor::Backend * backend)37 OctaveSession::OctaveSession ( Cantor::Backend* backend ) : Session ( backend),
38 m_process(nullptr),
39 m_prompt(QStringLiteral("CANTOR_OCTAVE_BACKEND_PROMPT:([0-9]+)> ")),
40 m_subprompt(QStringLiteral("CANTOR_OCTAVE_BACKEND_SUBPROMPT:([0-9]+)> ")),
41 m_previousPromptNumber(1),
42 m_syntaxError(false),
43 m_isIntegratedPlotsEnabled(false),
44 m_isIntegratedPlotsSettingsEnabled(false)
45 {
46     setVariableModel(new OctaveVariableModel(this));
47 }
48 
~OctaveSession()49 OctaveSession::~OctaveSession()
50 {
51     if (m_process)
52     {
53         m_process->kill();
54         m_process->deleteLater();
55         m_process = nullptr;
56     }
57 }
58 
login()59 void OctaveSession::login()
60 {
61     qDebug() << "login";
62     if (m_process)
63         return;
64 
65     emit loginStarted();
66 
67     m_process = new KProcess ( this );
68     QStringList args;
69     args << QLatin1String("--silent");
70     args << QLatin1String("--interactive");
71     args << QLatin1String("--persist");
72 
73     // Setting prompt and subprompt
74     args << QLatin1String("--eval");
75     args << QLatin1String("PS1('CANTOR_OCTAVE_BACKEND_PROMPT:\\#> ');");
76     args << QLatin1String("--eval");
77     args << QLatin1String("PS2('CANTOR_OCTAVE_BACKEND_SUBPROMPT:\\#> ');");
78 
79     // Add the cantor script directory to octave script search path
80     const QStringList& scriptDirs = locateAllCantorFiles(QLatin1String("octavebackend"), QStandardPaths::LocateDirectory);
81     if (scriptDirs.isEmpty())
82         qCritical() << "Octave script directory not found, needed for integrated plots";
83     else
84     {
85         for (const QString& dir : scriptDirs)
86             args << QLatin1String("--eval") << QString::fromLatin1("addpath \"%1\";").arg(dir);
87     }
88 
89     // Do not show extra text in help commands
90     args << QLatin1String("--eval");
91     args << QLatin1String("suppress_verbose_help_message(1);");
92 
93     m_process->setProgram ( OctaveSettings::path().toLocalFile(), args );
94     qDebug() << "starting " << m_process->program();
95     m_process->setOutputChannelMode ( KProcess::SeparateChannels );
96     m_process->start();
97     m_process->waitForStarted();
98 
99     connect ( m_process, SIGNAL (readyReadStandardOutput()), SLOT (readOutput()) );
100     connect ( m_process, SIGNAL (readyReadStandardError()), SLOT (readError()) );
101     connect ( m_process, SIGNAL (error(QProcess::ProcessError)), SLOT (processError()) );
102 
103     std::random_device rd;
104     std::mt19937 mt(rd());
105     std::uniform_int_distribution<int> rand_dist(0, 999999999);
106     m_plotFilePrefixPath =
107         QDir::tempPath()
108         + QLatin1String("/cantor_octave_")
109         + QString::number(m_process->pid())
110         + QLatin1String("_")
111         + QString::number(rand_dist(mt))
112         + QLatin1String("_");
113 
114     if(!OctaveSettings::self()->autorunScripts().isEmpty()){
115         QString autorunScripts = OctaveSettings::self()->autorunScripts().join(QLatin1String("\n"));
116 
117         evaluateExpression(autorunScripts, OctaveExpression::DeleteOnFinish, true);
118         updateVariables();
119     }
120     if (!m_worksheetPath.isEmpty())
121     {
122         static const QString mfilenameTemplate = QLatin1String(
123             "function retval = mfilename(arg_mem = \"\")\n"
124                 "type_info=typeinfo(arg_mem);\n"
125                 "if (strcmp(type_info, \"string\"))\n"
126                     "if (strcmp(arg_mem, \"fullpath\"))\n"
127                         "retval = \"%1\";\n"
128                     "elseif (strcmp(arg_mem, \"fullpathext\"))\n"
129                         "retval = \"%2\";\n"
130                     "else\n"
131                         "retval = \"script\";\n"
132                     "endif\n"
133                 "else\n"
134                     "error(\"wrong type argument '%s'\", type_info)\n"
135                 "endif\n"
136             "endfunction"
137         );
138         const QString& worksheetDirPath = QFileInfo(m_worksheetPath).absoluteDir().absolutePath();
139         const QString& worksheetPathWithoutExtension = m_worksheetPath.mid(0, m_worksheetPath.lastIndexOf(QLatin1Char('.')));
140 
141         evaluateExpression(QLatin1String("cd ")+worksheetDirPath, OctaveExpression::DeleteOnFinish, true);
142         evaluateExpression(mfilenameTemplate.arg(worksheetPathWithoutExtension, m_worksheetPath), OctaveExpression::DeleteOnFinish, true);
143     }
144 
145     changeStatus(Cantor::Session::Done);
146     emit loginDone();
147     qDebug()<<"login done";
148 }
149 
setWorksheetPath(const QString & path)150 void OctaveSession::setWorksheetPath(const QString& path)
151 {
152     m_worksheetPath = path;
153 }
154 
logout()155 void OctaveSession::logout()
156 {
157     qDebug()<<"logout";
158 
159     if(!m_process)
160         return;
161 
162     disconnect(m_process, nullptr, this, nullptr);
163 
164     if(status() == Cantor::Session::Running)
165         interrupt();
166 
167     m_process->write("exit\n");
168     qDebug()<<"send exit command to octave";
169 
170     if(!m_process->waitForFinished(1000))
171     {
172         m_process->kill();
173         qDebug()<<"octave still running, process kill enforced";
174     }
175     m_process->deleteLater();
176     m_process = nullptr;
177 
178     if (!m_plotFilePrefixPath.isEmpty())
179     {
180         int i = 0;
181         const QString& extension = OctaveExpression::plotExtensions[OctaveSettings::inlinePlotFormat()];
182         QString filename = m_plotFilePrefixPath + QString::number(i) + QLatin1String(".") + extension;
183         while (QFile::exists(filename))
184         {
185             QFile::remove(filename);
186             i++;
187             filename = m_plotFilePrefixPath + QString::number(i) + QLatin1String(".") + extension;
188         }
189     }
190 
191     expressionQueue().clear();
192 
193     m_output.clear();
194     m_previousPromptNumber = 1;
195     m_isIntegratedPlotsEnabled = false;
196     m_isIntegratedPlotsSettingsEnabled = false;
197 
198     Session::logout();
199 
200     qDebug()<<"logout done";
201 }
202 
interrupt()203 void OctaveSession::interrupt()
204 {
205     qDebug() << expressionQueue().size();
206     if(!expressionQueue().isEmpty())
207     {
208         qDebug()<<"interrupting " << expressionQueue().first()->command();
209         if(m_process && m_process->state() != QProcess::NotRunning)
210         {
211 #ifndef Q_OS_WIN
212             const int pid=m_process->pid();
213             kill(pid, SIGINT);
214 #else
215             ; //TODO: interrupt the process on windows
216 #endif
217         }
218         foreach (Cantor::Expression* expression, expressionQueue())
219             expression->setStatus(Cantor::Expression::Interrupted);
220         expressionQueue().clear();
221 
222         // Cleanup inner state and call octave prompt printing
223         // If we move this code for interruption to Session, we need add function for
224         // cleaning before setting Done status
225         m_output.clear();
226         m_process->write("\n");
227 
228         qDebug()<<"done interrupting";
229     }
230 
231     changeStatus(Cantor::Session::Done);
232 }
233 
processError()234 void OctaveSession::processError()
235 {
236     qDebug() << "processError";
237     emit error(m_process->errorString());
238 }
239 
evaluateExpression(const QString & command,Cantor::Expression::FinishingBehavior finishingBehavior,bool internal)240 Cantor::Expression* OctaveSession::evaluateExpression ( const QString& command, Cantor::Expression::FinishingBehavior finishingBehavior, bool internal )
241 {
242     if (!internal)
243         updateGraphicPackagesFromSettings();
244 
245     qDebug() << "evaluating: " << command;
246     OctaveExpression* expression = new OctaveExpression ( this, internal);
247     expression->setCommand ( command );
248     expression->setFinishingBehavior ( finishingBehavior );
249     expression->evaluate();
250 
251     return expression;
252 }
253 
runFirstExpression()254 void OctaveSession::runFirstExpression()
255 {
256     OctaveExpression* expression = static_cast<OctaveExpression*>(expressionQueue().first());
257     connect(expression, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentExpressionStatusChanged(Cantor::Expression::Status)));
258     QString command = expression->internalCommand();
259     expression->setStatus(Cantor::Expression::Computing);
260     if (isDoNothingCommand(command))
261         expression->setStatus(Cantor::Expression::Done);
262     else
263     {
264         m_process->write ( command.toLocal8Bit() );
265     }
266 }
267 
readError()268 void OctaveSession::readError()
269 {
270     qDebug() << "readError";
271     QString error = QString::fromLocal8Bit(m_process->readAllStandardError());
272     if (!expressionQueue().isEmpty() && !error.isEmpty())
273     {
274         OctaveExpression* const exp = static_cast<OctaveExpression*>(expressionQueue().first());
275         if (m_syntaxError)
276         {
277             m_syntaxError = false;
278             exp->parseError(i18n("Syntax Error"));
279         }
280         else
281             exp->parseError(error);
282 
283         m_output.clear();
284     }
285 }
286 
readOutput()287 void OctaveSession::readOutput()
288 {
289     qDebug() << "readOutput";
290     while (m_process->bytesAvailable() > 0)
291     {
292         QString line = QString::fromLocal8Bit(m_process->readLine());
293         qDebug()<<"start parsing " << "  " << line;
294         QRegularExpressionMatch match = m_prompt.match(line);
295         if (match.hasMatch())
296         {
297             const int promptNumber = match.captured(1).toInt();
298             // Add all text before prompt, if exists
299             m_output += QStringRef(&line, 0, match.capturedStart(0)).toString();
300             if (!expressionQueue().isEmpty())
301             {
302                 const QString& command = expressionQueue().first()->command();
303                 if (m_previousPromptNumber + 1 == promptNumber || isSpecialOctaveCommand(command))
304                 {
305                     if (!expressionQueue().isEmpty())
306                     {
307                         readError();
308                         static_cast<OctaveExpression*>(expressionQueue().first())->parseOutput(m_output);
309                     }
310                 }
311                 else
312                 {
313                     // Error command don't increase octave prompt number (usually, but not always)
314                     readError();
315                 }
316             }
317             m_previousPromptNumber = promptNumber;
318             m_output.clear();
319         }
320         else if ((match = m_subprompt.match(line)).hasMatch()
321                  && match.captured(1).toInt() == m_previousPromptNumber)
322         {
323             // User don't write finished octave statement (for example, write 'a = [1,2, ' only), so
324             // octave print subprompt and waits input finish.
325             m_syntaxError = true;
326             qDebug() << "subprompt catch";
327             m_process->write(")]'\"\n"); // force exit from subprompt
328             m_output.clear();
329         }
330         else
331             m_output += line;
332     }
333 }
334 
currentExpressionStatusChanged(Cantor::Expression::Status status)335 void OctaveSession::currentExpressionStatusChanged(Cantor::Expression::Status status)
336 {
337     qDebug() << "currentExpressionStatusChanged" << status << expressionQueue().first()->command();
338     switch (status)
339     {
340     case Cantor::Expression::Done:
341     case Cantor::Expression::Error:
342         finishFirstExpression();
343         break;
344 
345     default:
346         break;
347     }
348 }
349 
completionFor(const QString & cmd,int index)350 Cantor::CompletionObject* OctaveSession::completionFor ( const QString& cmd, int index )
351 {
352     return new OctaveCompletionObject ( cmd, index, this );
353 }
354 
syntaxHelpFor(const QString & cmd)355 Cantor::SyntaxHelpObject* OctaveSession::syntaxHelpFor ( const QString& cmd )
356 {
357     return new OctaveSyntaxHelpObject ( cmd, this );
358 }
359 
syntaxHighlighter(QObject * parent)360 QSyntaxHighlighter* OctaveSession::syntaxHighlighter ( QObject* parent )
361 {
362     return new OctaveHighlighter ( parent, this );
363 }
364 
runSpecificCommands()365 void OctaveSession::runSpecificCommands()
366 {
367     m_process->write("figure(1,'visible','off')");
368 }
369 
isDoNothingCommand(const QString & command)370 bool OctaveSession::isDoNothingCommand(const QString& command)
371 {
372     return PROMPT_UNCHANGEABLE_COMMAND.match(command).hasMatch()
373            || command.isEmpty() || command == QLatin1String("\n");
374 }
375 
isSpecialOctaveCommand(const QString & command)376 bool OctaveSession::isSpecialOctaveCommand(const QString& command)
377 {
378     return command.contains(QLatin1String("completion_matches"));
379 }
380 
isIntegratedPlotsEnabled() const381 bool OctaveSession::isIntegratedPlotsEnabled() const
382 {
383     return m_isIntegratedPlotsEnabled;
384 }
385 
plotFilePrefixPath() const386 QString OctaveSession::plotFilePrefixPath() const
387 {
388     return m_plotFilePrefixPath;
389 }
390 
updateGraphicPackagesFromSettings()391 void OctaveSession::updateGraphicPackagesFromSettings()
392 {
393     if (m_isIntegratedPlotsSettingsEnabled == OctaveSettings::integratePlots())
394         return;
395 
396     if (m_isIntegratedPlotsEnabled && OctaveSettings::integratePlots() == false)
397     {
398         updateEnabledGraphicPackages(QList<Cantor::GraphicPackage>());
399         m_isIntegratedPlotsEnabled = false;
400         m_isIntegratedPlotsSettingsEnabled = OctaveSettings::integratePlots();
401         return;
402     }
403     else if (!m_isIntegratedPlotsEnabled && OctaveSettings::integratePlots() == true)
404     {
405         bool isIntegratedPlots = OctaveSettings::integratePlots();
406         if (isIntegratedPlots)
407         {
408             QString filename = QDir::tempPath() + QLatin1String("/cantor_octave_plot_integration_test.txt");
409             QFile::remove(filename); // Remove previous file, if precents
410             int test_number = rand() % 1000;
411 
412             QStringList args;
413             args << QLatin1String("--no-init-file");
414             args << QLatin1String("--no-gui");
415             args << QLatin1String("--eval");
416             args << QString::fromLatin1("file_id = fopen('%1', 'w'); fdisp(file_id, %2); fclose(file_id);").arg(filename).arg(test_number);
417 
418             QString errorMsg;
419             isIntegratedPlots = Cantor::Backend::testProgramWritable(
420                 OctaveSettings::path().toLocalFile(),
421                 args,
422                 filename,
423                 QString::number(test_number),
424                 &errorMsg
425             );
426 
427             // If we in this branch, then isIntegratedPlots was true, but if it false now, then it means, that the writable test is failed
428             if (isIntegratedPlots == false)
429             {
430                 KMessageBox::error(nullptr,
431                     i18n("Plot integration test failed.")+
432                     QLatin1String("\n\n")+
433                     errorMsg+
434                     QLatin1String("\n\n")+
435                     i18n("The integration of plots will be disabled."),
436                     i18n("Cantor")
437                 );
438             }
439         }
440         m_isIntegratedPlotsEnabled = isIntegratedPlots;
441         m_isIntegratedPlotsSettingsEnabled = OctaveSettings::integratePlots();
442 
443         if (m_isIntegratedPlotsEnabled)
444             updateEnabledGraphicPackages(backend()->availableGraphicPackages());
445         else
446             updateEnabledGraphicPackages(QList<Cantor::GraphicPackage>());
447     }
448 }
449 
graphicPackageErrorMessage(QString packageId) const450 QString OctaveSession::graphicPackageErrorMessage(QString packageId) const
451 {
452     QString text;
453 
454     if (packageId == QLatin1String("gr")) {
455         return i18n(
456             "The plot integration doesn't work because Cantor found, that Octave can't create plots, "
457             "because there are no graphical backends for it: this conclusion was made on the basis of empty "
458             "output from available_graphics_toolkits() function. Looks like you should install some "
459             "additional OS packages, like gnuplot, fltk or qt for possibility to create plots."
460         );
461     }
462     return text;
463 }
464