OSDN Git Service

It's 2011 now.
[qt-creator-jp/qt-creator-jp.git] / share / qtcreator / qml / qmljsdebugger / jsdebuggeragent.cpp
1 /****************************************************************************
2 **
3 ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
6 **
7 ** This file is part of the QtSCriptTools module of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** No Commercial Usage
11 ** This file contains pre-release code and may not be distributed.
12 ** You may use this file in accordance with the terms and conditions
13 ** contained in the Technology Preview License Agreement accompanying
14 ** this package.
15 **
16 ** GNU Lesser General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU Lesser
18 ** General Public License version 2.1 as published by the Free Software
19 ** Foundation and appearing in the file LICENSE.LGPL included in the
20 ** packaging of this file.  Please review the following information to
21 ** ensure the GNU Lesser General Public License version 2.1 requirements
22 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
23 **
24 ** In addition, as a special exception, Nokia gives you certain additional
25 ** rights.  These rights are described in the Nokia Qt LGPL Exception
26 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
27 **
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
30 **
31 **
32 **
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "jsdebuggeragent.h"
43 #include "qt_private/qdeclarativedebughelper_p.h"
44
45 #include <QtCore/qdatetime.h>
46 #include <QtCore/qdebug.h>
47 #include <QtCore/qurl.h>
48 #include <QtCore/qcoreapplication.h>
49 #include <QtCore/qset.h>
50 #include <QtScript/qscriptcontextinfo.h>
51 #include <QtScript/qscriptengine.h>
52 #include <QtScript/qscriptvalueiterator.h>
53
54 namespace QmlJSDebugger {
55
56 enum JSDebuggerState
57 {
58     NoState,
59     SteppingIntoState,
60     SteppingOverState,
61     SteppingOutState,
62     StoppedState
63 };
64
65 struct JSAgentWatchData
66 {
67     QByteArray exp;
68     QByteArray name;
69     QByteArray value;
70     QByteArray type;
71     bool hasChildren;
72     quint64 objectId;
73 };
74
75 QDataStream &operator<<(QDataStream &s, const JSAgentWatchData &data)
76 {
77     return s << data.exp << data.name << data.value
78              << data.type << data.hasChildren << data.objectId;
79 }
80
81 struct JSAgentStackData
82 {
83     QByteArray functionName;
84     QByteArray fileName;
85     qint32 lineNumber;
86 };
87
88 QDataStream &operator<<(QDataStream &s, const JSAgentStackData &data)
89 {
90     return s << data.functionName << data.fileName << data.lineNumber;
91 }
92
93 struct JSAgentBreakpointData
94 {
95     QByteArray functionName;
96     QByteArray fileName;
97     qint32 lineNumber;
98 };
99
100 typedef QSet<JSAgentBreakpointData> JSAgentBreakpoints;
101
102 QDataStream &operator<<(QDataStream &s, const JSAgentBreakpointData &data)
103 {
104     return s << data.functionName << data.fileName << data.lineNumber;
105 }
106
107 QDataStream &operator>>(QDataStream &s, JSAgentBreakpointData &data)
108 {
109     return s >> data.functionName >> data.fileName >> data.lineNumber;
110 }
111
112 bool operator==(const JSAgentBreakpointData &b1, const JSAgentBreakpointData &b2)
113 {
114     return b1.lineNumber == b2.lineNumber && b1.fileName == b2.fileName;
115 }
116
117 uint qHash(const JSAgentBreakpointData &b)
118 {
119     return b.lineNumber ^ qHash(b.fileName);
120 }
121
122 class JSDebuggerAgentPrivate
123 {
124 public:
125     JSDebuggerAgentPrivate(JSDebuggerAgent *q)
126         : q(q), state(NoState)
127     {}
128
129     void continueExec();
130     void recordKnownObjects(const QList<JSAgentWatchData> &);
131     QList<JSAgentWatchData> getLocals(QScriptContext *);
132     void positionChange(qint64 scriptId, int lineNumber, int columnNumber);
133     QScriptEngine *engine() { return q->engine(); }
134     void stopped();
135     void messageReceived(const QByteArray &message);
136     void sendMessage(const QByteArray &message) { q->sendMessage(message); }
137
138 public:
139     JSDebuggerAgent *q;
140     JSDebuggerState state;
141     int stepDepth;
142     int stepCount;
143
144     QEventLoop loop;
145     QHash<qint64, QString> filenames;
146     JSAgentBreakpoints breakpoints;
147     QStringList watchExpressions;
148     QSet<qint64> knownObjectIds;
149 };
150
151 class SetupExecEnv
152 {
153 public:
154     SetupExecEnv(JSDebuggerAgentPrivate *a)
155         : agent(a),
156           previousState(a->state),
157           hadException(a->engine()->hasUncaughtException())
158     {
159         agent->state = StoppedState;
160     }
161
162     ~SetupExecEnv()
163     {
164         if (!hadException && agent->engine()->hasUncaughtException())
165             agent->engine()->clearExceptions();
166         agent->state = previousState;
167     }
168
169 private:
170     JSDebuggerAgentPrivate *agent;
171     JSDebuggerState previousState;
172     bool hadException;
173 };
174
175 static JSAgentWatchData fromScriptValue(const QString &expression,
176                                         const QScriptValue &value)
177 {
178     static const QString arrayStr = QCoreApplication::translate
179             ("Debugger::JSAgentWatchData", "[Array of length %1]");
180     static const QString undefinedStr = QCoreApplication::translate
181             ("Debugger::JSAgentWatchData", "<undefined>");
182
183     JSAgentWatchData data;
184     data.exp = expression.toUtf8();
185     data.name = data.exp;
186     data.hasChildren = false;
187     data.value = value.toString().toUtf8();
188     data.objectId = value.objectId();
189     if (value.isArray()) {
190         data.type = "Array";
191         data.value = arrayStr.arg(value.property("length").toString()).toUtf8();
192         data.hasChildren = true;
193     } else if (value.isBool()) {
194         data.type = "Bool";
195         // data.value = value.toBool() ? "true" : "false";
196     } else if (value.isDate()) {
197         data.type = "Date";
198         data.value = value.toDateTime().toString().toUtf8();
199     } else if (value.isError()) {
200         data.type = "Error";
201     } else if (value.isFunction()) {
202         data.type = "Function";
203     } else if (value.isUndefined()) {
204         data.type = undefinedStr.toUtf8();
205     } else if (value.isNumber()) {
206         data.type = "Number";
207     } else if (value.isRegExp()) {
208         data.type = "RegExp";
209     } else if (value.isString()) {
210         data.type = "String";
211     } else if (value.isVariant()) {
212         data.type = "Variant";
213     } else if (value.isQObject()) {
214         const QObject *obj = value.toQObject();
215         data.type = "Object";
216         data.value += '[';
217         data.value += obj->metaObject()->className();
218         data.value += ']';
219         data.hasChildren = true;
220     } else if (value.isObject()) {
221         data.type = "Object";
222         data.hasChildren = true;
223         data.value = "[Object]";
224     } else if (value.isNull()) {
225         data.type = "<null>";
226     } else {
227         data.type = "<unknown>";
228     }
229     return data;
230 }
231
232 static QList<JSAgentWatchData> expandObject(const QScriptValue &object)
233 {
234     QList<JSAgentWatchData> result;
235     QScriptValueIterator it(object);
236     QByteArray expPrefix = '@' + QByteArray::number(object.objectId(), 16) + "->";
237     while (it.hasNext()) {
238         it.next();
239         if (it.flags() & QScriptValue::SkipInEnumeration)
240             continue;
241         if (/*object.isQObject() &&*/ it.value().isFunction()) {
242             // Cosmetics: skip all functions and slot, there are too many of them,
243             // and it is not useful information in the debugger.
244             continue;
245         }
246         JSAgentWatchData data = fromScriptValue(it.name(), it.value());
247         data.exp.prepend(expPrefix);
248         result.append(data);
249     }
250     if (result.isEmpty()) {
251         JSAgentWatchData data;
252         data.name = "<no initialized data>";
253         data.hasChildren = false;
254         data.value = " ";
255         data.exp.prepend(expPrefix);
256         data.objectId = 0;
257         result.append(data);
258     }
259     return result;
260 }
261
262 void JSDebuggerAgentPrivate::recordKnownObjects(const QList<JSAgentWatchData>& list)
263 {
264     foreach (const JSAgentWatchData &data, list)
265         knownObjectIds << data.objectId;
266 }
267
268 QList<JSAgentWatchData> JSDebuggerAgentPrivate::getLocals(QScriptContext *ctx)
269 {
270     QList<JSAgentWatchData> locals;
271     if (ctx) {
272         QScriptValue activationObject = ctx->activationObject();
273         QScriptValue thisObject = ctx->thisObject();
274         locals = expandObject(activationObject);
275         if (thisObject.isObject()
276                 && thisObject.objectId() != engine()->globalObject().objectId())
277             locals.prepend(fromScriptValue("this", thisObject));
278         recordKnownObjects(locals);
279         knownObjectIds << activationObject.objectId();
280     }
281     return locals;
282 }
283
284 /*!
285   Constructs a new agent for the given \a engine. The agent will
286   report debugging-related events (e.g. step completion) to the given
287   \a backend.
288 */
289 JSDebuggerAgent::JSDebuggerAgent(QScriptEngine *engine)
290     : QDeclarativeDebugService("JSDebugger")
291     , QScriptEngineAgent(engine)
292     , d(new JSDebuggerAgentPrivate(this))
293 {}
294
295 JSDebuggerAgent::JSDebuggerAgent(QDeclarativeEngine *engine)
296     : QDeclarativeDebugService("JSDebugger")
297     , QScriptEngineAgent(QDeclarativeDebugHelper::getScriptEngine(engine))
298     , d(new JSDebuggerAgentPrivate(this))
299 {}
300
301 /*!
302   Destroys this QScriptDebuggerAgent.
303 */
304 JSDebuggerAgent::~JSDebuggerAgent()
305 {
306     delete d;
307 }
308
309 /*!
310   \reimp
311 */
312 void JSDebuggerAgent::scriptLoad(qint64 id, const QString &program,
313                                  const QString &fileName, int)
314 {
315     Q_UNUSED(program);
316     d->filenames.insert(id, QUrl(fileName).toLocalFile());
317 }
318
319 /*!
320   \reimp
321 */
322 void JSDebuggerAgent::scriptUnload(qint64 id)
323 {
324     d->filenames.remove(id);
325 }
326
327 /*!
328   \reimp
329 */
330 void JSDebuggerAgent::contextPush()
331 {
332 }
333
334 /*!
335   \reimp
336 */
337 void JSDebuggerAgent::contextPop()
338 {
339 }
340
341 /*!
342   \reimp
343 */
344 void JSDebuggerAgent::functionEntry(qint64 scriptId)
345 {
346     Q_UNUSED(scriptId);
347     d->stepDepth++;
348 }
349
350 /*!
351   \reimp
352 */
353 void JSDebuggerAgent::functionExit(qint64 scriptId, const QScriptValue &returnValue)
354 {
355     Q_UNUSED(scriptId);
356     Q_UNUSED(returnValue);
357     d->stepDepth--;
358 }
359
360 /*!
361   \reimp
362 */
363 void JSDebuggerAgent::positionChange(qint64 scriptId, int lineNumber, int columnNumber)
364 {
365     d->positionChange(scriptId, lineNumber, columnNumber);
366 }
367
368 void JSDebuggerAgentPrivate::positionChange(qint64 scriptId, int lineNumber, int columnNumber)
369 {
370     Q_UNUSED(columnNumber);
371
372     if (state == StoppedState)
373         return; //no re-entrency
374
375     // check breakpoints
376     if (!breakpoints.isEmpty()) {
377         QHash<qint64, QString>::const_iterator it = filenames.constFind(scriptId);
378         QScriptContext *ctx = engine()->currentContext();
379         QScriptContextInfo info(ctx);
380         if (it == filenames.constEnd()) {
381             // It is possible that the scripts are loaded before the agent is attached
382             QString filename = QUrl(info.fileName()).toLocalFile();
383
384             JSAgentStackData frame;
385             frame.functionName = info.functionName().toUtf8();
386
387             QPair<QString, qint32> key = qMakePair(filename, lineNumber);
388             it = filenames.insert(scriptId, filename);
389         }
390         JSAgentBreakpointData bp;
391         bp.fileName = it->toUtf8();
392         bp.lineNumber = lineNumber;
393         if (breakpoints.contains(bp)) {
394             stopped();
395             return;
396         }
397     }
398
399     switch (state) {
400     case NoState:
401     case StoppedState:
402         // Do nothing
403         break;
404     case SteppingOutState:
405         if (stepDepth >= 0)
406             break;
407         //fallthough
408     case SteppingOverState:
409         if (stepDepth > 0)
410             break;
411         //fallthough
412     case SteppingIntoState:
413         stopped();
414         break;
415     }
416
417 }
418
419 /*!
420   \reimp
421 */
422 void JSDebuggerAgent::exceptionThrow(qint64 scriptId,
423                                      const QScriptValue &exception,
424                                      bool hasHandler)
425 {
426     Q_UNUSED(scriptId);
427     Q_UNUSED(exception);
428     Q_UNUSED(hasHandler);
429 //    qDebug() << Q_FUNC_INFO << exception.toString() << hasHandler;
430 #if 0 //sometimes, we get exceptions that we should just ignore.
431     if (!hasHandler && state != StoppedState)
432         stopped(true, exception);
433 #endif
434 }
435
436 /*!
437   \reimp
438 */
439 void JSDebuggerAgent::exceptionCatch(qint64 scriptId, const QScriptValue &exception)
440 {
441     Q_UNUSED(scriptId);
442     Q_UNUSED(exception);
443 }
444
445 bool JSDebuggerAgent::supportsExtension(Extension extension) const
446 {
447     return extension == QScriptEngineAgent::DebuggerInvocationRequest;
448 }
449
450 QVariant JSDebuggerAgent::extension(Extension extension, const QVariant &argument)
451 {
452     if (extension == QScriptEngineAgent::DebuggerInvocationRequest) {
453         d->stopped();
454         return QVariant();
455     }
456     return QScriptEngineAgent::extension(extension, argument);
457 }
458
459 void JSDebuggerAgent::messageReceived(const QByteArray &message)
460 {
461     d->messageReceived(message);
462 }
463
464 void JSDebuggerAgentPrivate::messageReceived(const QByteArray &message)
465 {
466     QDataStream ds(message);
467     QByteArray command;
468     ds >> command;
469     if (command == "BREAKPOINTS") {
470         ds >> breakpoints;
471         //qDebug() << "BREAKPOINTS";
472         //foreach (const JSAgentBreakpointData &bp, breakpoints)
473         //    qDebug() << "BREAKPOINT: " << bp.fileName << bp.lineNumber;
474     } else if (command == "WATCH_EXPRESSIONS") {
475         ds >> watchExpressions;
476     } else if (command == "STEPOVER") {
477         stepDepth = 0;
478         state = SteppingOverState;
479         continueExec();
480     } else if (command == "STEPINTO" || command == "INTERRUPT") {
481         stepDepth = 0;
482         state = SteppingIntoState;
483         continueExec();
484     } else if (command == "STEPOUT") {
485         stepDepth = 0;
486         state = SteppingOutState;
487         continueExec();
488     } else if (command == "CONTINUE") {
489         state = NoState;
490         continueExec();
491     } else if (command == "EXEC") {
492         SetupExecEnv execEnv(this);
493
494         QByteArray id;
495         QString expr;
496         ds >> id >> expr;
497
498         JSAgentWatchData data = fromScriptValue(expr, engine()->evaluate(expr));
499         knownObjectIds << data.objectId;
500
501         QByteArray reply;
502         QDataStream rs(&reply, QIODevice::WriteOnly);
503         rs << QByteArray("RESULT") << id << data;
504         sendMessage(reply);
505     } else if (command == "EXPAND") {
506         SetupExecEnv execEnv(this);
507
508         QByteArray requestId;
509         quint64 objectId;
510         ds >> requestId >> objectId;
511         QScriptValue v;
512         if (knownObjectIds.contains(objectId))
513             v = engine()->objectById(objectId);
514
515         QList<JSAgentWatchData> result = expandObject(v);
516         recordKnownObjects(result);
517
518         QByteArray reply;
519         QDataStream rs(&reply, QIODevice::WriteOnly);
520         rs << QByteArray("EXPANDED") << requestId << result;
521         sendMessage(reply);
522
523     } else if (command == "ACTIVATE_FRAME") {
524         SetupExecEnv execEnv(this);
525
526         int frameId;
527         ds >> frameId;
528
529         int deep = 0;
530         QScriptContext *ctx = engine()->currentContext();
531         while (ctx && deep < frameId) {
532             ctx = ctx->parentContext();
533             deep++;
534         }
535
536         QList<JSAgentWatchData> locals = getLocals(ctx);
537
538         QByteArray reply;
539         QDataStream rs(&reply, QIODevice::WriteOnly);
540         rs << QByteArray("LOCALS") << frameId << locals;
541         sendMessage(reply);
542     } else if (command == "SET_PROPERTY") {
543         SetupExecEnv execEnv(this);
544
545         QByteArray id;
546         qint64 objectId;
547         QString property;
548         QString value;
549         ds >> id >> objectId >> property >> value;
550
551         if (knownObjectIds.contains(objectId)) {
552             QScriptValue object;
553             object = engine()->objectById(objectId);
554
555             if (object.isObject()) {
556                 QScriptValue result = engine()->evaluate(value);
557                 object.setProperty(property, result);
558             }
559         }
560
561         //TODO: feedback
562     } else if (command == "PING") {
563         int ping;
564         ds >> ping;
565         QByteArray reply;
566         QDataStream rs(&reply, QIODevice::WriteOnly);
567         rs << QByteArray("PONG") << ping;
568         sendMessage(reply);
569     } else {
570         qDebug() << Q_FUNC_INFO << "Unknown command" << command;
571     }
572
573     q->baseMessageReceived(message);
574 }
575
576 void JSDebuggerAgentPrivate::stopped()
577 {
578     bool becauseOfException = false;
579     const QScriptValue &exception = QScriptValue();
580
581     knownObjectIds.clear();
582     state = StoppedState;
583     QList<JSAgentStackData> backtrace;
584
585     for (QScriptContext* ctx = engine()->currentContext(); ctx; ctx = ctx->parentContext()) {
586         QScriptContextInfo info(ctx);
587
588         JSAgentStackData frame;
589         frame.functionName = info.functionName().toUtf8();
590         if (frame.functionName.isEmpty()) {
591             if (ctx->parentContext()) {
592                 switch (info.functionType()) {
593                 case QScriptContextInfo::ScriptFunction:
594                     frame.functionName = "<anonymous>";
595                     break;
596                 case QScriptContextInfo::NativeFunction:
597                     frame.functionName = "<native>";
598                     break;
599                 case QScriptContextInfo::QtFunction:
600                 case QScriptContextInfo::QtPropertyFunction:
601                     frame.functionName = "<native slot>";
602                     break;
603                 }
604             } else {
605                 frame.functionName = "<global>";
606             }
607         }
608         frame.lineNumber = info.lineNumber();
609         // if the line number is unknown, fallback to the function line number
610         if (frame.lineNumber == -1)
611             frame.lineNumber = info.functionStartLineNumber();
612         frame.fileName = QUrl(info.fileName()).toLocalFile().toUtf8();
613         backtrace.append(frame);
614     }
615     QList<JSAgentWatchData> watches;
616     foreach (const QString &expr, watchExpressions)
617         watches << fromScriptValue(expr, engine()->evaluate(expr));
618     recordKnownObjects(watches);
619
620     QList<JSAgentWatchData> locals = getLocals(engine()->currentContext());
621
622     if (!becauseOfException) {
623         // Clear any exceptions occurred during locals evaluation.
624         engine()->clearExceptions();
625     }
626
627     QByteArray reply;
628     QDataStream rs(&reply, QIODevice::WriteOnly);
629     rs << QByteArray("STOPPED") << backtrace << watches << locals
630        << becauseOfException << exception.toString();
631     sendMessage(reply);
632
633     loop.exec(QEventLoop::ExcludeUserInputEvents);
634 }
635
636 void JSDebuggerAgentPrivate::continueExec()
637 {
638     loop.quit();
639 }
640
641 void JSDebuggerAgent::statusChanged(Status status)
642 {
643     engine()->setAgent((status == QDeclarativeDebugService::Enabled) ? this : 0);
644 }
645
646 void JSDebuggerAgent::baseMessageReceived(const QByteArray &message)
647 {
648     QDeclarativeDebugService::messageReceived(message);
649 }
650
651 } // namespace QmlJSDebugger