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"