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