File indexing completed on 2023-05-30 09:03:12
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