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

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2012 Filipe Saraiva <filipe@kde.org>
0004     SPDX-FileCopyrightText: 2015 Minh Ngo <minh@fedoraproject.org>
0005 */
0006 
0007 #include <defaultvariablemodel.h>
0008 #include <backend.h>
0009 #include "pythonsession.h"
0010 #include "pythonexpression.h"
0011 #include "pythonvariablemodel.h"
0012 #include "pythonhighlighter.h"
0013 #include "pythoncompletionobject.h"
0014 #include "pythonkeywords.h"
0015 #include "pythonutils.h"
0016 #include "settings.h"
0017 
0018 #include <random>
0019 
0020 #include <QDebug>
0021 #include <QDir>
0022 #include <QStandardPaths>
0023 #include <QFileInfo>
0024 
0025 #include <KLocalizedString>
0026 #include <KMessageBox>
0027 
0028 #ifndef Q_OS_WIN
0029 #include <signal.h>
0030 #endif
0031 
0032 const QChar recordSep(30);
0033 const QChar unitSep(31);
0034 const QChar messageEnd = 29;
0035 
0036 PythonSession::PythonSession(Cantor::Backend* backend) : Session(backend)
0037 {
0038     setVariableModel(new PythonVariableModel(this));
0039 }
0040 
0041 PythonSession::~PythonSession()
0042 {
0043     if (m_process) {
0044         disconnect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError);
0045         m_process->kill();
0046         m_process->deleteLater();
0047         m_process = nullptr;
0048     }
0049 }
0050 
0051 void PythonSession::login()
0052 {
0053     qDebug()<<"login";
0054     emit loginStarted();
0055 
0056     if (m_process)
0057         m_process->deleteLater();
0058 
0059     m_process = new QProcess(this);
0060     m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
0061 
0062 #ifdef Q_OS_WIN
0063     const QString& serverExecutablePath = QStandardPaths::findExecutable(QLatin1String("cantor_pythonserver.exe"));
0064     // On Windows QProcess can't handle paths with spaces, so add escaping
0065     m_process->start(QLatin1String("\"") + serverExecutablePath + QLatin1String("\""));
0066 #else
0067     const QString& serverExecutablePath = QStandardPaths::findExecutable(QLatin1String("cantor_pythonserver"));
0068     m_process->start(serverExecutablePath);
0069 #endif
0070 
0071     m_process->waitForStarted();
0072     m_process->waitForReadyRead();
0073     QTextStream stream(m_process->readAllStandardOutput());
0074 
0075     const QString& readyStatus = QString::fromLatin1("ready");
0076     while (m_process->state() == QProcess::Running)
0077     {
0078         const QString& rl = stream.readLine();
0079         if (rl == readyStatus)
0080             break;
0081     }
0082 
0083     connect(m_process, &QProcess::readyReadStandardOutput, this, &PythonSession::readOutput);
0084     connect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError);
0085 
0086     sendCommand(QLatin1String("login"));
0087     QString dir;
0088     if (!m_worksheetPath.isEmpty())
0089         dir = QFileInfo(m_worksheetPath).absoluteDir().absolutePath();
0090     sendCommand(QLatin1String("setFilePath"), QStringList() << m_worksheetPath << dir);
0091 
0092     std::random_device rd;
0093     std::mt19937 mt(rd());
0094     std::uniform_int_distribution<int> rand_dist(0, 999999999);
0095     m_plotFilePrefixPath =
0096         QDir::tempPath()
0097         + QLatin1String("/cantor_python_")
0098         + QString::number(m_process->processId())
0099         + QLatin1String("_")
0100         + QString::number(rand_dist(mt))
0101         + QLatin1String("_");
0102 
0103     m_plotFileCounter = 0;
0104     evaluateExpression(QLatin1String("__cantor_plot_global_counter__ = 0"), Cantor::Expression::DeleteOnFinish, true);
0105 
0106     const QStringList& scripts = PythonSettings::autorunScripts();
0107     if(!scripts.isEmpty()){
0108         QString autorunScripts = scripts.join(QLatin1String("\n"));
0109         evaluateExpression(autorunScripts, Cantor::Expression::DeleteOnFinish, true);
0110         variableModel()->update();
0111     }
0112 
0113     changeStatus(Session::Done);
0114     emit loginDone();
0115 }
0116 
0117 void PythonSession::logout()
0118 {
0119     if (!m_process)
0120         return;
0121 
0122     if (m_process->exitStatus() != QProcess::CrashExit && m_process->error() != QProcess::WriteError)
0123         sendCommand(QLatin1String("exit"));
0124 
0125     if(m_process->state() == QProcess::Running && !m_process->waitForFinished(1000))
0126     {
0127         disconnect(m_process, &QProcess::errorOccurred, this, &PythonSession::reportServerProcessError);
0128         m_process->kill();
0129         qDebug()<<"cantor_python server still running, process kill enforced";
0130     }
0131     m_process->deleteLater();
0132     m_process = nullptr;
0133 
0134     if (!m_plotFilePrefixPath.isEmpty())
0135     {
0136         for (int i = 0; i < m_plotFileCounter; i++)
0137             QFile::remove(m_plotFilePrefixPath + QString::number(i) + QLatin1String(".png"));
0138         m_plotFilePrefixPath.clear();
0139         m_plotFileCounter = 0;
0140     }
0141 
0142     qDebug()<<"logout";
0143     Session::logout();
0144 }
0145 
0146 void PythonSession::interrupt()
0147 {
0148     if(!expressionQueue().isEmpty())
0149     {
0150         qDebug()<<"interrupting " << expressionQueue().first()->command();
0151         if(m_process && m_process->state() != QProcess::NotRunning)
0152         {
0153 #ifndef Q_OS_WIN
0154             const int pid = m_process->processId();
0155             kill(pid, SIGINT);
0156 #else
0157             ; //TODO: interrupt the process on windows
0158 #endif
0159         }
0160         for (auto* expression : expressionQueue())
0161             expression->setStatus(Cantor::Expression::Interrupted);
0162         expressionQueue().clear();
0163 
0164         m_output.clear();
0165 
0166         qDebug()<<"done interrupting";
0167     }
0168 
0169     changeStatus(Cantor::Session::Done);
0170 }
0171 
0172 Cantor::Expression* PythonSession::evaluateExpression(const QString& cmd, Cantor::Expression::FinishingBehavior behave, bool internal)
0173 {
0174     qDebug() << "evaluating: " << cmd;
0175     auto* expr = new PythonExpression(this, internal);
0176     expr->setFinishingBehavior(behave);
0177     expr->setCommand(cmd);
0178     expr->evaluate();
0179 
0180     return expr;
0181 }
0182 
0183 QSyntaxHighlighter* PythonSession::syntaxHighlighter(QObject* parent)
0184 {
0185     return new PythonHighlighter(parent, this);
0186 }
0187 
0188 Cantor::CompletionObject* PythonSession::completionFor(const QString& command, int index)
0189 {
0190     return new PythonCompletionObject(command, index, this);
0191 }
0192 
0193 void PythonSession::runFirstExpression()
0194 {
0195     if (expressionQueue().isEmpty())
0196         return;
0197 
0198     auto* expr = expressionQueue().first();
0199     const QString& command = expr->internalCommand();
0200     qDebug() << "run first expression" << command;
0201     expr->setStatus(Cantor::Expression::Computing);
0202 
0203     if (expr->isInternal() && command.startsWith(QLatin1String("%variables ")))
0204     {
0205         const QString arg = command.section(QLatin1String(" "), 1);
0206         sendCommand(QLatin1String("model"), QStringList(arg));
0207     }
0208     else
0209         sendCommand(QLatin1String("code"), QStringList(expr->internalCommand()));
0210 }
0211 
0212 void PythonSession::sendCommand(const QString& command, const QStringList arguments) const
0213 {
0214     qDebug() << "send command: " << command << arguments;
0215     const QString& message = command + recordSep + arguments.join(unitSep) + messageEnd;
0216     m_process->write(message.toLocal8Bit());
0217 }
0218 
0219 void PythonSession::readOutput()
0220 {
0221     while (m_process->bytesAvailable() > 0)
0222     {
0223         const QByteArray& bytes = m_process->readAll();
0224         m_output.append(QString::fromUtf8(bytes));
0225     }
0226 
0227     qDebug() << "m_output: " << m_output;
0228 
0229     if (!m_output.contains(messageEnd))
0230         return;
0231 
0232     const QStringList packages = m_output.split(messageEnd, QString::SkipEmptyParts);
0233     if (m_output.endsWith(messageEnd))
0234         m_output.clear();
0235     else
0236         m_output = m_output.section(messageEnd, -1);
0237 
0238     for (const QString& message: packages)
0239     {
0240         if (expressionQueue().isEmpty())
0241             break;
0242 
0243         const QString& output = message.section(unitSep, 0, 0);
0244         const QString& error = message.section(unitSep, 1, 1);
0245         bool isError = message.section(unitSep, 2, 2).toInt();
0246         auto* expr = expressionQueue().first();
0247         if (isError)
0248         {
0249             if(error.isEmpty()){
0250                 expr->parseOutput(output);
0251             } else {
0252                 expr->parseError(error);
0253             }
0254         }
0255         else
0256         {
0257             static_cast<PythonExpression*>(expr)->parseWarning(error);
0258             expr->parseOutput(output);
0259         }
0260         finishFirstExpression(true);
0261     }
0262 }
0263 
0264 void PythonSession::setWorksheetPath(const QString& path)
0265 {
0266     m_worksheetPath = path;
0267 }
0268 
0269 void PythonSession::reportServerProcessError(QProcess::ProcessError serverError)
0270 {
0271     switch(serverError)
0272     {
0273         case QProcess::Crashed:
0274             emit error(i18n("Cantor Python server stopped working."));
0275             break;
0276 
0277         case QProcess::FailedToStart:
0278             emit error(i18n("Failed to start Cantor python server."));
0279             break;
0280 
0281         default:
0282             emit error(i18n("Communication with Cantor python server failed for unknown reasons."));
0283             break;
0284     }
0285     reportSessionCrash();
0286 }
0287 
0288 int& PythonSession::plotFileCounter()
0289 {
0290     return m_plotFileCounter;
0291 }
0292 
0293 QString PythonSession::plotFilePrefixPath()
0294 {
0295     return m_plotFilePrefixPath;
0296 }
0297 
0298 void PythonSession::updateGraphicPackagesFromSettings()
0299 {
0300     updateEnabledGraphicPackages(backend()->availableGraphicPackages(), m_plotFilePrefixPath);
0301 }
0302 
0303 QString PythonSession::graphicPackageErrorMessage(QString packageId) const
0304 {
0305     if (packageId == QLatin1String("matplotlib"))
0306     {
0307         return i18n(
0308             "For using integrated graphics with Matplotlib package, you need to install \"matplotlib\" python package first."
0309         );
0310     }
0311     else if (packageId == QLatin1String("plotly"))
0312     {
0313         return i18n(
0314             "For using integrated graphic with Plot.ly, you need to install \"plotly\" python package and special Plot.ly-compatible "
0315             "\"orca\" executable. See \"Static Image Export\" article in Plot.ly documentation for details."
0316         );
0317     }
0318     return QString();
0319 }
0320