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
24const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
25 "com.highfidelity.experimental.enableExtendedJSExceptions"
26};
27
28const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
29const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
30
31// engine-aware JS Error copier and factory
32QScriptValue 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
66QScriptValue 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
89QScriptValue 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
147QString 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
176QScriptValue 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
243QScriptValue 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}
251QString Lambda::toString() const {
252 return QString("[Lambda%1]").arg(data.isValid() ? " " + data.toString() : data.toString());
253}
254
255Lambda::~Lambda() {
256#ifdef DEBUG_JS_LAMBDA_FUNCS
257 qDebug() << "~Lambda" << "this" << this;
258#endif
259}
260
261Lambda::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}
267QScriptValue Lambda::call() {
268 return operation(engine->currentContext(), engine);
269}
270
271// BaseScriptEngine::ExceptionEmitter
272
273void BaseScriptEngine::_emitUnhandledException(const QScriptValue& exception) {
274 emit unhandledException(exception);
275}
276BaseScriptEngine::ExceptionEmitter::ExceptionEmitter(BaseScriptEngine* engine, const QString& debugName)
277 : _engine(engine), _debugName(debugName) {
278}
279bool BaseScriptEngine::ExceptionEmitter::hasPending() {
280 return _engine->hasUncaughtException();
281}
282QScriptValue BaseScriptEngine::ExceptionEmitter::pending() {
283 return _engine->cloneUncaughtException(_debugName);
284}
285QScriptValue BaseScriptEngine::ExceptionEmitter::consume() {
286 if (hasPending()) {
287 _consumedException = pending();
288 _engine->clearExceptions();
289 }
290 return _consumedException;
291}
292bool BaseScriptEngine::ExceptionEmitter::wouldEmit() {
293 return !_engine->isEvaluating() && _engine->hasUncaughtException();
294}
295BaseScriptEngine::ExceptionEmitter::~ExceptionEmitter() {
296 if (wouldEmit()) {
297 _engine->_emitUnhandledException(consume());
298 }
299}
300
301
302#ifdef DEBUG_JS
303void 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