History

separate out supporting changes into 21114-part2 branch

humbletim committed 9 years ago

Commit b1595621 parent 9b0bee910 files · +833 −223Browse files

Showing the changes to libraries/script-engine/src/BaseScriptEngine.cpp (1 of 10 files). Show all

Unified · Split

libraries/script-engine/src/BaseScriptEngine.cpp+321−0
@@ -0,0 +1,321 @@
1+//
2+// BaseScriptEngine.cpp
3+// libraries/script-engine/src
4+//
5+// Created by Timothy Dedischew on 02/01/17.
6+// Copyright 2017 High Fidelity, Inc.
7+//
8+// Distributed under the Apache License, Version 2.0.
9+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
10+//
11+
12+#include "BaseScriptEngine.h"
13+
14+#include <QtCore/QString>
15+#include <QtCore/QThread>
16+#include <QtCore/QUrl>
17+#include <QtScript/QScriptValue>
18+#include <QtScript/QScriptValueIterator>
19+#include <QtScript/QScriptContextInfo>
20+
21+#include "ScriptEngineLogging.h"
22+#include "Profile.h"
23+
24+const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
25+ "com.highfidelity.experimental.enableExtendedJSExceptions"
26+};
27+
28+const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
29+const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
30+
31+// engine-aware JS Error copier and factory
32+QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
33+ auto other = _other;
34+ if (other.isString()) {
35+ other = newObject();
36+ other.setProperty("message", _other.toString());
37+ }
38+ auto proto = globalObject().property(type);
39+ if (!proto.isFunction()) {
40+ proto = globalObject().property(other.prototype().property("constructor").property("name").toString());
41+ }
42+ if (!proto.isFunction()) {
43+#ifdef DEBUG_JS_EXCEPTIONS
44+ qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
45+#endif
46+ proto = globalObject().property("Error");
47+ }
48+ if (other.engine() != this) {
49+ // JS Objects are parented to a specific script engine instance
50+ // -- this effectively ~clones it locally by routing through a QVariant and back
51+ other = toScriptValue(other.toVariant());
52+ }
53+ // ~ var err = new Error(other.message)
54+ auto err = proto.construct(QScriptValueList({other.property("message")}));
55+
56+ // transfer over any existing properties
57+ QScriptValueIterator it(other);
58+ while (it.hasNext()) {
59+ it.next();
60+ err.setProperty(it.name(), it.value());
61+ }
62+ return err;
63+}
64+
65+// check syntax and when there are issues returns an actual "SyntaxError" with the details
66+QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
67+ const auto syntaxCheck = checkSyntax(sourceCode);
68+ if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
69+ auto err = globalObject().property("SyntaxError")
70+ .construct(QScriptValueList({syntaxCheck.errorMessage()}));
71+ err.setProperty("fileName", fileName);
72+ err.setProperty("lineNumber", syntaxCheck.errorLineNumber());
73+ err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber());
74+ err.setProperty("stack", currentContext()->backtrace().join(SCRIPT_BACKTRACE_SEP));
75+ {
76+ const auto error = syntaxCheck.errorMessage();
77+ const auto line = QString::number(syntaxCheck.errorLineNumber());
78+ const auto column = QString::number(syntaxCheck.errorColumnNumber());
79+ // for compatibility with legacy reporting
80+ const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column);
81+ err.setProperty("formatted", message);
82+ }
83+ return err;
84+ }
85+ return undefinedValue();
86+}
87+
88+// this pulls from the best available information to create a detailed snapshot of the current exception
89+QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
90+ if (!hasUncaughtException()) {
91+ return QScriptValue();
92+ }
93+ auto exception = uncaughtException();
94+ // ensure the error object is engine-local
95+ auto err = makeError(exception);
96+
97+ // not sure why Qt does't offer uncaughtExceptionFileName -- but the line number
98+ // on its own is often useless/wrong if arbitrarily married to a filename.
99+ // when the error object already has this info, it seems to be the most reliable
100+ auto fileName = exception.property("fileName").toString();
101+ auto lineNumber = exception.property("lineNumber").toInt32();
102+
103+ // the backtrace, on the other hand, seems most reliable taken from uncaughtExceptionBacktrace
104+ auto backtrace = uncaughtExceptionBacktrace();
105+ if (backtrace.isEmpty()) {
106+ // fallback to the error object
107+ backtrace = exception.property("stack").toString().split(SCRIPT_BACKTRACE_SEP);
108+ }
109+ // the ad hoc "detail" property can be used now to embed additional clues
110+ auto detail = exception.property("detail").toString();
111+ if (detail.isEmpty()) {
112+ detail = extraDetail;
113+ } else if (!extraDetail.isEmpty()) {
114+ detail += "(" + extraDetail + ")";
115+ }
116+ if (lineNumber <= 0) {
117+ lineNumber = uncaughtExceptionLineNumber();
118+ }
119+ if (fileName.isEmpty()) {
120+ // climb the stack frames looking for something useful to display
121+ for(auto ctx = currentContext(); ctx && fileName.isEmpty(); ctx = ctx->parentContext()) {
122+ QScriptContextInfo info { ctx };
123+ if (!info.fileName().isEmpty()) {
124+ // take fileName:lineNumber as a pair
125+ fileName = info.fileName();
126+ lineNumber = info.lineNumber();
127+ if (backtrace.isEmpty()) {
128+ backtrace = ctx->backtrace();
129+ }
130+ break;
131+ }
132+ }
133+ }
134+ err.setProperty("fileName", fileName);
135+ err.setProperty("lineNumber", lineNumber );
136+ err.setProperty("detail", detail);
137+ err.setProperty("stack", backtrace.join(SCRIPT_BACKTRACE_SEP));
138+
139+#ifdef DEBUG_JS_EXCEPTIONS
140+ err.setProperty("_fileName", exception.property("fileName").toString());
141+ err.setProperty("_stack", uncaughtExceptionBacktrace().join(SCRIPT_BACKTRACE_SEP));
142+ err.setProperty("_lineNumber", uncaughtExceptionLineNumber());
143+#endif
144+ return err;
145+}
146+
147+QString BaseScriptEngine::formatException(const QScriptValue& exception) {
148+ QString note { "UncaughtException" };
149+ QString result;
150+
151+ if (!exception.isObject()) {
152+ return result;
153+ }
154+ const auto message = exception.toString();
155+ const auto fileName = exception.property("fileName").toString();
156+ const auto lineNumber = exception.property("lineNumber").toString();
157+ const auto stacktrace = exception.property("stack").toString();
158+
159+ if (_enableExtendedJSExceptions.get()) {
160+ // This setting toggles display of the hints now being added during the loading process.
161+ // Example difference:
162+ // [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
163+ // [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
164+ if (exception.property("detail").isValid()) {
165+ note += " " + exception.property("detail").toString();
166+ }
167+ }
168+
169+ result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber);
170+ if (!stacktrace.isEmpty()) {
171+ result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace);
172+ }
173+ return result;
174+}
175+
176+QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
177+ PROFILE_RANGE(script, "evaluateInClosure");
178+ if (QThread::currentThread() != thread()) {
179+ qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
180+ // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
181+ return QScriptValue();
182+ }
183+
184+ //_debugDump("evaluateInClosure.closure", closure);
185+
186+ const auto fileName = program.fileName();
187+ const auto shortName = QUrl(fileName).fileName();
188+
189+ QScriptValue result;
190+ QScriptValue oldGlobal;
191+ auto global = closure.property("global");
192+ if (global.isObject()) {
193+#ifdef DEBUG_JS
194+ qCDebug(scriptengine) << " setting global = closure.global" << shortName;
195+#endif
196+ oldGlobal = globalObject();
197+ setGlobalObject(global);
198+ }
199+
200+ auto ctx = pushContext();
201+
202+ auto thiz = closure.property("this");
203+ if (thiz.isObject()) {
204+#ifdef DEBUG_JS
205+ qCDebug(scriptengine) << " setting this = closure.this" << shortName;
206+#endif
207+ ctx->setThisObject(thiz);
208+ }
209+
210+ ctx->pushScope(closure);
211+#ifdef DEBUG_JS
212+ qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
213+#endif
214+ {
215+ ExceptionEmitter tryCatch(this, __FUNCTION__);
216+ result = BaseScriptEngine::evaluate(program);
217+ if (tryCatch.hasPending()) {
218+ auto err = tryCatch.pending();
219+#ifdef DEBUG_JS_EXCEPTIONS
220+ qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
221+ err.setProperty("_result", result);
222+#endif
223+ result = err;
224+ }
225+ }
226+#ifdef DEBUG_JS
227+ qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
228+#endif
229+ popContext();
230+
231+ if (oldGlobal.isValid()) {
232+#ifdef DEBUG_JS
233+ qCDebug(scriptengine) << " restoring global" << shortName;
234+#endif
235+ setGlobalObject(oldGlobal);
236+ }
237+
238+ return result;
239+}
240+
241+// Lambda
242+
243+QScriptValue BaseScriptEngine::newLambdaFunction(std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
244+ auto lambda = new Lambda(this, operation, data);
245+ auto object = newQObject(lambda, ownership);
246+ auto call = object.property("call");
247+ call.setPrototype(object); // ctx->callee().prototype() === Lambda QObject
248+ call.setData(data); // ctx->callee().data() will === data param
249+ return call;
250+}
251+QString Lambda::toString() const {
252+ return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString());
253+}
254+
255+Lambda::~Lambda() {
256+#ifdef DEBUG_JS_LAMBDA_FUNCS
257+ qDebug() << "~Lambda" << "this" << this;
258+#endif
259+}
260+
261+Lambda::Lambda(QScriptEngine *engine, std::function<QScriptValue(QScriptContext *, QScriptEngine*)> operation, QScriptValue data)
262+ : engine(engine), operation(operation), data(data) {
263+#ifdef DEBUG_JS_LAMBDA_FUNCS
264+ qDebug() << "Lambda" << data.toString();
265+#endif
266+}
267+QScriptValue Lambda::call() {
268+ return operation(engine->currentContext(), engine);
269+}
270+
271+// BaseScriptEngine::ExceptionEmitter
272+
273+void BaseScriptEngine::_emitUnhandledException(const QScriptValue& exception) {
274+ emit unhandledException(exception);
275+}
276+BaseScriptEngine::ExceptionEmitter::ExceptionEmitter(BaseScriptEngine* engine, const QString& debugName)
277+ : _engine(engine), _debugName(debugName) {
278+}
279+bool BaseScriptEngine::ExceptionEmitter::hasPending() {
280+ return _engine->hasUncaughtException();
281+}
282+QScriptValue BaseScriptEngine::ExceptionEmitter::pending() {
283+ return _engine->cloneUncaughtException(_debugName);
284+}
285+QScriptValue BaseScriptEngine::ExceptionEmitter::consume() {
286+ if (hasPending()) {
287+ _consumedException = pending();
288+ _engine->clearExceptions();
289+ }
290+ return _consumedException;
291+}
292+bool BaseScriptEngine::ExceptionEmitter::wouldEmit() {
293+ return !_engine->isEvaluating() && _engine->hasUncaughtException();
294+}
295+BaseScriptEngine::ExceptionEmitter::~ExceptionEmitter() {
296+ if (wouldEmit()) {
297+ _engine->_emitUnhandledException(consume());
298+ }
299+}
300+
301+
302+#ifdef DEBUG_JS
303+void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
304+ if (!header.isEmpty()) {
305+ qCDebug(scriptengine) << header;
306+ }
307+ if (!object.isObject()) {
308+ qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
309+ return;
310+ }
311+ QScriptValueIterator it(object);
312+ while (it.hasNext()) {
313+ it.next();
314+ qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
315+ }
316+ if (!footer.isEmpty()) {
317+ qCDebug(scriptengine) << footer;
318+ }
319+}
320+#endif
321+