File indexing completed on 2024-04-28 04:38:40

0001 /*
0002     SPDX-FileCopyrightText: 2009 Andreas Pakulat <apaku@gmx.de>
0003     SPDX-FileCopyrightText: 2010 Milian Wolff <mail@milianw.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "externalscriptjob.h"
0009 
0010 #include "externalscriptitem.h"
0011 #include "externalscriptplugin.h"
0012 #include <debug.h>
0013 
0014 #include <QFileInfo>
0015 #include <QApplication>
0016 
0017 #include <KProcess>
0018 #include <KLocalizedString>
0019 #include <KShell>
0020 
0021 #include <KTextEditor/Document>
0022 #include <KTextEditor/View>
0023 
0024 #include <outputview/outputmodel.h>
0025 #include <outputview/outputdelegate.h>
0026 #include <util/processlinemaker.h>
0027 
0028 #include <interfaces/icore.h>
0029 #include <interfaces/idocumentcontroller.h>
0030 #include <interfaces/iprojectcontroller.h>
0031 #include <interfaces/iproject.h>
0032 #include <interfaces/iuicontroller.h>
0033 #include <project/projectmodel.h>
0034 #include <serialization/indexedstring.h>
0035 #include <sublime/message.h>
0036 #include <util/path.h>
0037 
0038 using namespace KDevelop;
0039 
0040 ExternalScriptJob::ExternalScriptJob(ExternalScriptItem* item, const QUrl& url, ExternalScriptPlugin* parent)
0041     : KDevelop::OutputJob(parent)
0042     , m_proc(nullptr)
0043     , m_lineMaker(nullptr)
0044     , m_outputMode(item->outputMode())
0045     , m_inputMode(item->inputMode())
0046     , m_errorMode(item->errorMode())
0047     , m_filterMode(item->filterMode())
0048     , m_document(nullptr)
0049     , m_url(url)
0050     , m_selectionRange(KTextEditor::Range::invalid())
0051     , m_showOutput(item->showOutput())
0052 {
0053     qCDebug(PLUGIN_EXTERNALSCRIPT) << "creating external script job";
0054 
0055     setCapabilities(Killable);
0056     setStandardToolView(KDevelop::IOutputView::RunView);
0057     setBehaviours(KDevelop::IOutputView::AllowUserClose | KDevelop::IOutputView::AutoScroll);
0058 
0059     auto* model = new KDevelop::OutputModel;
0060     model->setFilteringStrategy(static_cast<KDevelop::OutputModel::OutputFilterStrategy>(m_filterMode));
0061     setModel(model);
0062 
0063     setDelegate(new KDevelop::OutputDelegate);
0064 
0065     // also merge when error mode "equals" output mode
0066     if ((m_outputMode == ExternalScriptItem::OutputInsertAtCursor
0067          && m_errorMode == ExternalScriptItem::ErrorInsertAtCursor) ||
0068         (m_outputMode == ExternalScriptItem::OutputReplaceDocument
0069          && m_errorMode == ExternalScriptItem::ErrorReplaceDocument) ||
0070         (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrDocument
0071          && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrDocument) ||
0072         (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrInsertAtCursor
0073          && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrInsertAtCursor) ||
0074         // also these two otherwise they clash...
0075         (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrInsertAtCursor
0076          && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrDocument) ||
0077         (m_outputMode == ExternalScriptItem::OutputReplaceSelectionOrDocument
0078          && m_errorMode == ExternalScriptItem::ErrorReplaceSelectionOrInsertAtCursor)) {
0079         m_errorMode = ExternalScriptItem::ErrorMergeOutput;
0080     }
0081 
0082     KTextEditor::View* view = KDevelop::ICore::self()->documentController()->activeTextDocumentView();
0083 
0084     if (m_outputMode != ExternalScriptItem::OutputNone || m_inputMode != ExternalScriptItem::InputNone
0085         || m_errorMode != ExternalScriptItem::ErrorNone) {
0086         if (!view) {
0087             const QString messageText =
0088                 i18n("Cannot run script '%1' since it tries to access "
0089                      "the editor contents but no document is open.", item->text());
0090             auto* message = new Sublime::Message(messageText, Sublime::Message::Error);
0091             ICore::self()->uiController()->postMessage(message);
0092             return;
0093         }
0094 
0095         m_document = view->document();
0096 
0097         connect(m_document, &KTextEditor::Document::aboutToClose, this, [&] {
0098             kill();
0099         });
0100 
0101         m_selectionRange = view->selectionRange();
0102         m_cursorPosition = view->cursorPosition();
0103     }
0104 
0105     if (item->saveMode() == ExternalScriptItem::SaveCurrentDocument && view) {
0106         view->document()->save();
0107     } else if (item->saveMode() == ExternalScriptItem::SaveAllDocuments) {
0108         const auto documents = KDevelop::ICore::self()->documentController()->openDocuments();
0109         for (KDevelop::IDocument* doc : documents) {
0110             doc->save();
0111         }
0112     }
0113 
0114     QString command = item->command();
0115     QString workingDir = item->workingDirectory();
0116 
0117     if (item->performParameterReplacement())
0118         command.replace(QLatin1String("%i"), QString::number(QCoreApplication::applicationPid()));
0119 
0120     if (!m_url.isEmpty()) {
0121         const QUrl url = m_url;
0122 
0123         KDevelop::ProjectFolderItem* folder = nullptr;
0124         if (KDevelop::ICore::self()->projectController()->findProjectForUrl(url)) {
0125             QList<KDevelop::ProjectFolderItem*> folders =
0126                 KDevelop::ICore::self()->projectController()->findProjectForUrl(url)->foldersForPath(KDevelop::IndexedString(
0127                                                                                                          url));
0128             if (!folders.isEmpty()) {
0129                 folder = folders.first();
0130             }
0131         }
0132 
0133         if (folder) {
0134             if (folder->path().isLocalFile() && workingDir.isEmpty()) {
0135                 ///TODO: make configurable, use fallback to project dir
0136                 workingDir = folder->path().toLocalFile();
0137             }
0138 
0139             ///TODO: make those placeholders escapeable
0140             if (item->performParameterReplacement()) {
0141                 command.replace(QLatin1String("%d"), KShell::quoteArg(m_url.toString(QUrl::PreferLocalFile)));
0142 
0143                 if (KDevelop::IProject* project =
0144                         KDevelop::ICore::self()->projectController()->findProjectForUrl(m_url)) {
0145                     command.replace(QLatin1String("%p"), project->path().pathOrUrl());
0146                 }
0147             }
0148         } else {
0149             if (m_url.isLocalFile() && workingDir.isEmpty()) {
0150                 ///TODO: make configurable, use fallback to project dir
0151                 workingDir = view->document()->url().adjusted(QUrl::RemoveFilename).toLocalFile();
0152             }
0153 
0154             ///TODO: make those placeholders escapeable
0155             if (item->performParameterReplacement()) {
0156                 command.replace(QLatin1String("%u"), KShell::quoteArg(m_url.toString()));
0157 
0158                 ///TODO: does that work with remote files?
0159                 QFileInfo info(m_url.toString(QUrl::PreferLocalFile));
0160 
0161                 command.replace(QLatin1String("%f"), KShell::quoteArg(info.filePath()));
0162                 command.replace(QLatin1String("%b"), KShell::quoteArg(info.baseName()));
0163                 command.replace(QLatin1String("%n"), KShell::quoteArg(info.fileName()));
0164                 command.replace(QLatin1String("%d"), KShell::quoteArg(info.path()));
0165 
0166                 if (view->document()) {
0167                     command.replace(QLatin1String("%c"),
0168                                     KShell::quoteArg(QString::number(view->cursorPosition().column())));
0169                     command.replace(QLatin1String("%l"), KShell::quoteArg(QString::number(
0170                                                                               view->cursorPosition().line())));
0171                 }
0172 
0173                 if (view->document() && view->selection()) {
0174                     command.replace(QLatin1String("%s"), KShell::quoteArg(view->selectionText()));
0175                 }
0176 
0177                 if (KDevelop::IProject* project =
0178                         KDevelop::ICore::self()->projectController()->findProjectForUrl(m_url)) {
0179                     command.replace(QLatin1String("%p"), project->path().pathOrUrl());
0180                 }
0181             }
0182         }
0183     }
0184 
0185     m_proc = new KProcess(this);
0186     if (!workingDir.isEmpty()) {
0187         m_proc->setWorkingDirectory(workingDir);
0188     }
0189     m_lineMaker = new ProcessLineMaker(m_proc, this);
0190     connect(m_lineMaker, &ProcessLineMaker::receivedStdoutLines,
0191             model, &OutputModel::appendLines);
0192     connect(m_lineMaker, &ProcessLineMaker::receivedStdoutLines,
0193             this, &ExternalScriptJob::receivedStdoutLines);
0194     connect(m_lineMaker, &ProcessLineMaker::receivedStderrLines,
0195             model, &OutputModel::appendLines);
0196     connect(m_lineMaker, &ProcessLineMaker::receivedStderrLines,
0197             this, &ExternalScriptJob::receivedStderrLines);
0198     connect(m_proc, &QProcess::errorOccurred,
0199             this, &ExternalScriptJob::processError);
0200     connect(m_proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
0201             this, &ExternalScriptJob::processFinished);
0202 
0203     // Now setup the process parameters
0204     qCDebug(PLUGIN_EXTERNALSCRIPT) << "setting command:" << command;
0205 
0206     if (m_errorMode == ExternalScriptItem::ErrorMergeOutput) {
0207         m_proc->setOutputChannelMode(KProcess::MergedChannels);
0208     } else {
0209         m_proc->setOutputChannelMode(KProcess::SeparateChannels);
0210     }
0211     m_proc->setShellCommand(command);
0212 
0213     setObjectName(command);
0214 }
0215 
0216 void ExternalScriptJob::start()
0217 {
0218     qCDebug(PLUGIN_EXTERNALSCRIPT) << "launching?" << m_proc;
0219 
0220     if (m_proc) {
0221         if (m_showOutput) {
0222             startOutput();
0223         }
0224         appendLine(i18n("Running external script: %1", m_proc->program().join(QLatin1Char(' '))));
0225         m_proc->start();
0226 
0227         if (m_inputMode != ExternalScriptItem::InputNone) {
0228             QString inputText;
0229 
0230             switch (m_inputMode) {
0231             case ExternalScriptItem::InputNone:
0232                 // do nothing;
0233                 break;
0234             case ExternalScriptItem::InputSelectionOrNone:
0235                 if (m_selectionRange.isValid()) {
0236                     inputText = m_document->text(m_selectionRange);
0237                 } // else nothing
0238                 break;
0239             case ExternalScriptItem::InputSelectionOrDocument:
0240                 if (m_selectionRange.isValid()) {
0241                     inputText = m_document->text(m_selectionRange);
0242                 } else {
0243                     inputText = m_document->text();
0244                 }
0245                 break;
0246             case ExternalScriptItem::InputDocument:
0247                 inputText = m_document->text();
0248                 break;
0249             }
0250 
0251             ///TODO: what to do with the encoding here?
0252             ///      maybe ask Christoph for what kate returns...
0253             m_proc->write(inputText.toUtf8());
0254 
0255             m_proc->closeWriteChannel();
0256         }
0257     } else {
0258         qCWarning(PLUGIN_EXTERNALSCRIPT) << "No process, something went wrong when creating the job";
0259         // No process means we've returned early on from the constructor, some bad error happened
0260         emitResult();
0261     }
0262 }
0263 
0264 bool ExternalScriptJob::doKill()
0265 {
0266     if (m_proc) {
0267         m_proc->kill();
0268         appendLine(i18n("*** Killed Application ***"));
0269     }
0270 
0271     return true;
0272 }
0273 
0274 void ExternalScriptJob::processFinished(int exitCode, QProcess::ExitStatus status)
0275 {
0276     m_lineMaker->flushBuffers();
0277 
0278     if (exitCode == 0 && status == QProcess::NormalExit) {
0279         if (m_outputMode != ExternalScriptItem::OutputNone) {
0280             if (!m_stdout.isEmpty()) {
0281                 QString output = m_stdout.join(QLatin1Char('\n'));
0282                 switch (m_outputMode) {
0283                 case ExternalScriptItem::OutputNone:
0284                     // do nothing;
0285                     break;
0286                 case ExternalScriptItem::OutputCreateNewFile:
0287                     KDevelop::ICore::self()->documentController()->openDocumentFromText(output);
0288                     break;
0289                 case ExternalScriptItem::OutputInsertAtCursor:
0290                     m_document->insertText(m_cursorPosition, output);
0291                     break;
0292                 case ExternalScriptItem::OutputReplaceSelectionOrInsertAtCursor:
0293                     if (m_selectionRange.isValid()) {
0294                         m_document->replaceText(m_selectionRange, output);
0295                     } else {
0296                         m_document->insertText(m_cursorPosition, output);
0297                     }
0298                     break;
0299                 case ExternalScriptItem::OutputReplaceSelectionOrDocument:
0300                     if (m_selectionRange.isValid()) {
0301                         m_document->replaceText(m_selectionRange, output);
0302                     } else {
0303                         m_document->setText(output);
0304                     }
0305                     break;
0306                 case ExternalScriptItem::OutputReplaceDocument:
0307                     m_document->setText(output);
0308                     break;
0309                 }
0310             }
0311         }
0312         if (m_errorMode != ExternalScriptItem::ErrorNone && m_errorMode != ExternalScriptItem::ErrorMergeOutput) {
0313             QString output = m_stderr.join(QLatin1Char('\n'));
0314 
0315             if (!output.isEmpty()) {
0316                 switch (m_errorMode) {
0317                 case ExternalScriptItem::ErrorNone:
0318                 case ExternalScriptItem::ErrorMergeOutput:
0319                     // do nothing;
0320                     break;
0321                 case ExternalScriptItem::ErrorCreateNewFile:
0322                     KDevelop::ICore::self()->documentController()->openDocumentFromText(output);
0323                     break;
0324                 case ExternalScriptItem::ErrorInsertAtCursor:
0325                     m_document->insertText(m_cursorPosition, output);
0326                     break;
0327                 case ExternalScriptItem::ErrorReplaceSelectionOrInsertAtCursor:
0328                     if (m_selectionRange.isValid()) {
0329                         m_document->replaceText(m_selectionRange, output);
0330                     } else {
0331                         m_document->insertText(m_cursorPosition, output);
0332                     }
0333                     break;
0334                 case ExternalScriptItem::ErrorReplaceSelectionOrDocument:
0335                     if (m_selectionRange.isValid()) {
0336                         m_document->replaceText(m_selectionRange, output);
0337                     } else {
0338                         m_document->setText(output);
0339                     }
0340                     break;
0341                 case ExternalScriptItem::ErrorReplaceDocument:
0342                     m_document->setText(output);
0343                     break;
0344                 }
0345             }
0346         }
0347 
0348         appendLine(i18n("*** Exited normally ***"));
0349     } else {
0350         if (status == QProcess::NormalExit)
0351             appendLine(i18n("*** Exited with return code: %1 ***", QString::number(exitCode)));
0352         else
0353         if (error() == KJob::KilledJobError)
0354             appendLine(i18n("*** Process aborted ***"));
0355         else
0356             appendLine(i18n("*** Crashed with return code: %1 ***", QString::number(exitCode)));
0357     }
0358 
0359     qCDebug(PLUGIN_EXTERNALSCRIPT) << "Process done";
0360 
0361     emitResult();
0362 }
0363 
0364 void ExternalScriptJob::processError(QProcess::ProcessError error)
0365 {
0366     if (error == QProcess::FailedToStart) {
0367         setError(-1);
0368         QString errmsg =  i18n("*** Could not start program '%1'. Make sure that the "
0369                                "path is specified correctly ***", m_proc->program().join(QLatin1Char(' ')));
0370         appendLine(errmsg);
0371         setErrorText(errmsg);
0372         emitResult();
0373     }
0374 
0375     qCDebug(PLUGIN_EXTERNALSCRIPT) << "Process error";
0376 }
0377 
0378 void ExternalScriptJob::appendLine(const QString& l)
0379 {
0380     if (KDevelop::OutputModel* m = model()) {
0381         m->appendLine(l);
0382     }
0383 }
0384 
0385 KDevelop::OutputModel* ExternalScriptJob::model()
0386 {
0387     return qobject_cast<KDevelop::OutputModel*>(OutputJob::model());
0388 }
0389 
0390 void ExternalScriptJob::receivedStderrLines(const QStringList& lines)
0391 {
0392     m_stderr += lines;
0393 }
0394 
0395 void ExternalScriptJob::receivedStdoutLines(const QStringList& lines)
0396 {
0397     m_stdout += lines;
0398 }
0399 
0400 #include "moc_externalscriptjob.cpp"