1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Copyright (C) 2019 Jochen Ulrich <jochenulrich@t-online.de>
5 ** Contact: https://www.qt.io/licensing/
6 **
7 ** This file is part of Qbs.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** Commercial License Usage
11 ** Licensees holding valid commercial Qt licenses may use this file in
12 ** accordance with the commercial license agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and The Qt Company. For licensing terms
15 ** and conditions see https://www.qt.io/terms-conditions. For further
16 ** information use the contact form at https://www.qt.io/contact-us.
17 **
18 ** GNU Lesser General Public License Usage
19 ** Alternatively, this file may be used under the terms of the GNU Lesser
20 ** General Public License version 3 as published by the Free Software
21 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
22 ** packaging of this file. Please review the following information to
23 ** ensure the GNU Lesser General Public License version 3 requirements
24 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
25 **
26 ** GNU General Public License Usage
27 ** Alternatively, this file may be used under the terms of the GNU
28 ** General Public License version 2.0 or (at your option) the GNU General
29 ** Public license version 3 or any later version approved by the KDE Free
30 ** Qt Foundation. The licenses are as published by the Free Software
31 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
32 ** included in the packaging of this file. Please review the following
33 ** information to ensure the GNU General Public License requirements will
34 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
35 ** https://www.gnu.org/licenses/gpl-3.0.html.
36 **
37 ** $QT_END_LICENSE$
38 **
39 ****************************************************************************/
40 
41 #include "jscommandexecutor.h"
42 
43 #include "artifact.h"
44 #include "buildgraph.h"
45 #include "rulecommands.h"
46 #include "transformer.h"
47 
48 #include <language/language.h>
49 #include <language/preparescriptobserver.h>
50 #include <language/resolvedfilecontext.h>
51 #include <language/scriptengine.h>
52 #include <logging/logger.h>
53 #include <tools/codelocation.h>
54 #include <tools/error.h>
55 #include <tools/qbsassert.h>
56 #include <tools/qttools.h>
57 
58 #include <QtCore/qeventloop.h>
59 #include <QtCore/qpointer.h>
60 #include <QtCore/qthread.h>
61 #include <QtCore/qtimer.h>
62 
63 namespace qbs {
64 namespace Internal {
65 
66 struct JavaScriptCommandResult
67 {
68     bool success = false;
69     QString errorMessage;
70     CodeLocation errorLocation;
71 };
72 
73 class JsCommandExecutorThreadObject : public QObject
74 {
75     Q_OBJECT
76 public:
JsCommandExecutorThreadObject(Logger logger)77     JsCommandExecutorThreadObject(Logger logger)
78         : m_logger(std::move(logger))
79         , m_scriptEngine(nullptr)
80     {
81     }
82 
result() const83     const JavaScriptCommandResult &result() const
84     {
85         return m_result;
86     }
87 
cancel(const qbs::ErrorInfo & reason)88     void cancel(const qbs::ErrorInfo &reason)
89     {
90         m_result.success = !reason.hasError();
91         m_result.errorMessage = reason.toString();
92         if (m_scriptEngine)
93             m_scriptEngine->abortEvaluation();
94         m_cancelled = true;
95     }
96 
97 signals:
98     void finished();
99 
100 public:
start(const JavaScriptCommand * cmd,Transformer * transformer)101     void start(const JavaScriptCommand *cmd, Transformer *transformer)
102     {
103         if (m_cancelled) {
104             emit finished();
105             return;
106         }
107 
108         m_running = true;
109         try {
110             doStart(cmd, transformer);
111         } catch (const qbs::ErrorInfo &error) {
112             setError(error.toString(), cmd->codeLocation());
113         }
114 
115         m_running = false;
116         emit finished();
117     }
118 
119 private:
doStart(const JavaScriptCommand * cmd,Transformer * transformer)120     void doStart(const JavaScriptCommand *cmd, Transformer *transformer)
121     {
122         m_result.success = true;
123         m_result.errorMessage.clear();
124         ScriptEngine * const scriptEngine = provideScriptEngine();
125         QScriptValue scope = scriptEngine->newObject();
126         scope.setPrototype(scriptEngine->globalObject());
127         m_scriptEngine->clearRequestedProperties();
128         setupScriptEngineForFile(scriptEngine,
129                                  transformer->rule->prepareScript.fileContext(), scope,
130                                  ObserveMode::Enabled);
131 
132         QScriptValue importScopeForSourceCode;
133         if (!cmd->scopeName().isEmpty())
134             importScopeForSourceCode = scope.property(cmd->scopeName());
135 
136         setupScriptEngineForProduct(scriptEngine, transformer->product().get(),
137                                     transformer->rule->module.get(), scope, true);
138         transformer->setupInputs(scope);
139         transformer->setupOutputs(scope);
140         transformer->setupExplicitlyDependsOn(scope);
141 
142         for (QVariantMap::const_iterator it = cmd->properties().constBegin();
143                 it != cmd->properties().constEnd(); ++it) {
144             scope.setProperty(it.key(), scriptEngine->toScriptValue(it.value()));
145         }
146 
147         scriptEngine->setGlobalObject(scope);
148         if (importScopeForSourceCode.isObject())
149             scriptEngine->currentContext()->pushScope(importScopeForSourceCode);
150         scriptEngine->evaluate(cmd->sourceCode());
151         scriptEngine->releaseResourcesOfScriptObjects();
152         if (importScopeForSourceCode.isObject())
153             scriptEngine->currentContext()->popScope();
154         scriptEngine->setGlobalObject(scope.prototype());
155         transformer->propertiesRequestedInCommands
156                 += scriptEngine->propertiesRequestedInScript();
157         unite(transformer->propertiesRequestedFromArtifactInCommands,
158               scriptEngine->propertiesRequestedFromArtifact());
159         const std::vector<QString> &importFilesUsedInCommand
160                 = scriptEngine->importedFilesUsedInScript();
161         transformer->importedFilesUsedInCommands.insert(
162                     transformer->importedFilesUsedInCommands.cend(),
163                     importFilesUsedInCommand.cbegin(), importFilesUsedInCommand.cend());
164         transformer->depsRequestedInCommands.add(scriptEngine->productsWithRequestedDependencies());
165         transformer->artifactsMapRequestedInCommands.unite(scriptEngine->requestedArtifacts());
166         for (const ResolvedProduct * const p : scriptEngine->requestedExports()) {
167             transformer->exportedModulesAccessedInCommands.insert(
168                         std::make_pair(p->uniqueName(), p->exportedModule));
169         }
170         scriptEngine->clearRequestedProperties();
171         if (scriptEngine->hasUncaughtException()) {
172             // ### We don't know the line number of the command's sourceCode property assignment.
173             setError(scriptEngine->uncaughtException().toString(), cmd->codeLocation());
174         }
175     }
176 
setError(const QString & errorMessage,const CodeLocation & codeLocation)177     void setError(const QString &errorMessage, const CodeLocation &codeLocation)
178     {
179         m_result.success = false;
180         m_result.errorMessage = errorMessage;
181         m_result.errorLocation = codeLocation;
182     }
183 
provideScriptEngine()184     ScriptEngine *provideScriptEngine()
185     {
186         if (!m_scriptEngine)
187             m_scriptEngine = ScriptEngine::create(m_logger, EvalContext::JsCommand, this);
188         return m_scriptEngine;
189     }
190 
191     Logger m_logger;
192     ScriptEngine *m_scriptEngine;
193     JavaScriptCommandResult m_result;
194     bool m_running = false;
195     bool m_cancelled = false;
196 };
197 
198 
JsCommandExecutor(const Logger & logger,QObject * parent)199 JsCommandExecutor::JsCommandExecutor(const Logger &logger, QObject *parent)
200     : AbstractCommandExecutor(logger, parent)
201     , m_thread(new QThread(this))
202     , m_objectInThread(new JsCommandExecutorThreadObject(logger))
203     , m_running(false)
204 {
205     qRegisterMetaType<Transformer *>("Transformer *");
206     qRegisterMetaType<const JavaScriptCommand *>("const JavaScriptCommand *");
207 
208     m_objectInThread->moveToThread(m_thread);
209     connect(m_objectInThread, &JsCommandExecutorThreadObject::finished,
210             this, &JsCommandExecutor::onJavaScriptCommandFinished);
211     connect(this, &JsCommandExecutor::startRequested,
212             m_objectInThread, &JsCommandExecutorThreadObject::start);
213 }
214 
~JsCommandExecutor()215 JsCommandExecutor::~JsCommandExecutor()
216 {
217     waitForFinished();
218     m_thread->quit();
219     m_thread->wait();
220     delete m_objectInThread;
221 }
222 
doReportCommandDescription(const QString & productName)223 void JsCommandExecutor::doReportCommandDescription(const QString &productName)
224 {
225     if ((m_echoMode == CommandEchoModeCommandLine
226          || m_echoMode == CommandEchoModeCommandLineWithEnvironment)
227             && !command()->extendedDescription().isEmpty()) {
228         emit reportCommandDescription(command()->highlight(), command()->extendedDescription());
229         return;
230     }
231 
232     AbstractCommandExecutor::doReportCommandDescription(productName);
233 }
234 
waitForFinished()235 void JsCommandExecutor::waitForFinished()
236 {
237     if (!m_running)
238         return;
239     QEventLoop loop;
240     connect(this, &AbstractCommandExecutor::finished, &loop, &QEventLoop::quit);
241     loop.exec();
242 }
243 
doStart()244 bool JsCommandExecutor::doStart()
245 {
246     QBS_ASSERT(!m_running, return false);
247 
248     if (dryRun() && !command()->ignoreDryRun()) {
249         QTimer::singleShot(0, this, [this] { emit finished(); }); // Don't call back on the caller.
250         return false;
251     }
252 
253     m_thread->start();
254     m_running = true;
255     emit startRequested(jsCommand(), transformer());
256     return true;
257 }
258 
cancel(const qbs::ErrorInfo & reason)259 void JsCommandExecutor::cancel(const qbs::ErrorInfo &reason)
260 {
261     if (m_running && (!dryRun() || command()->ignoreDryRun()))
262         QTimer::singleShot(0, m_objectInThread, [objectInThread = QPointer<JsCommandExecutorThreadObject>{m_objectInThread}, reason] {
263             if (objectInThread)
264                 objectInThread->cancel(reason);
265         });
266 }
267 
onJavaScriptCommandFinished()268 void JsCommandExecutor::onJavaScriptCommandFinished()
269 {
270     m_running = false;
271     const JavaScriptCommandResult &result = m_objectInThread->result();
272     ErrorInfo err;
273     if (!result.success) {
274         logger().qbsDebug() << "JS context:\n" << jsCommand()->properties();
275         logger().qbsDebug() << "JS code:\n" << jsCommand()->sourceCode();
276         err.append(result.errorMessage);
277         // ### We don't know the line number of the command's sourceCode property assignment.
278         err.appendBacktrace(QStringLiteral("JavaScriptCommand.sourceCode"));
279         err.appendBacktrace(QStringLiteral("Rule.prepare"), result.errorLocation);
280     }
281     emit finished(err);
282 }
283 
jsCommand() const284 const JavaScriptCommand *JsCommandExecutor::jsCommand() const
285 {
286     return static_cast<const JavaScriptCommand *>(command());
287 }
288 
289 } // namespace Internal
290 } // namespace qbs
291 
292 #include "jscommandexecutor.moc"
293