Debugger: Don't register v8 callback until client is attached
[profile/ivi/qtdeclarative.git] / src / declarative / debugger / qv8debugservice.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 QtDeclarative module of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:LGPL$
10 ** GNU Lesser General Public License Usage
11 ** This file may be used under the terms of the GNU Lesser General Public
12 ** License version 2.1 as published by the Free Software Foundation and
13 ** appearing in the file LICENSE.LGPL included in the packaging of this
14 ** file. Please review the following information to ensure the GNU Lesser
15 ** General Public License version 2.1 requirements will be met:
16 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
17 **
18 ** In addition, as a special exception, Nokia gives you certain additional
19 ** rights. These rights are described in the Nokia Qt LGPL Exception
20 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
21 **
22 ** GNU General Public License Usage
23 ** Alternatively, this file may be used under the terms of the GNU General
24 ** Public License version 3.0 as published by the Free Software Foundation
25 ** and appearing in the file LICENSE.GPL included in the packaging of this
26 ** file. Please review the following information to ensure the GNU General
27 ** Public License version 3.0 requirements will be met:
28 ** http://www.gnu.org/copyleft/gpl.html.
29 **
30 ** Other Usage
31 ** Alternatively, this file may be used in accordance with the terms and
32 ** conditions contained in a signed written agreement between you and Nokia.
33 **
34 **
35 **
36 **
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41
42 #include "qv8debugservice_p.h"
43 #include "qdeclarativedebugservice_p_p.h"
44 #include <private/qv8debug_p.h>
45 #include <private/qv8engine_p.h>
46 #include <private/qdeclarativeengine_p.h>
47
48 #include <QtCore/QHash>
49 #include <QtCore/QFileInfo>
50
51 QT_BEGIN_NAMESPACE
52
53 struct SignalHandlerData
54 {
55     QString functionName;
56     bool enabled;
57 };
58
59 Q_GLOBAL_STATIC(QV8DebugService, v8ServiceInstance)
60
61 // DebugMessageHandler will call back already when the QV8DebugService constructor is
62 // running, we therefore need a plain pointer.
63 static QV8DebugService *v8ServiceInstancePtr = 0;
64
65 void DebugMessageHandler(const v8::Debug::Message& message)
66 {
67     v8::DebugEvent event = message.GetEvent();
68
69     if (event != v8::Break && event != v8::Exception &&
70             event != v8::AfterCompile && event != v8::BeforeCompile) {
71         return;
72     }
73
74     const QString response(QV8Engine::toStringStatic(
75                                   message.GetJSON()));
76
77     v8ServiceInstancePtr->debugMessageHandler(response, message.WillStartRunning());
78
79     if (event == v8::AfterCompile) {
80         v8ServiceInstancePtr->appendSourcePath(response);
81     } //TODO::v8::Exception
82 }
83
84 class QV8DebugServicePrivate : public QDeclarativeDebugServicePrivate
85 {
86 public:
87     QV8DebugServicePrivate()
88         : connectReceived(false)
89         , scheduleBreak(false)
90         , debuggerThreadIsolate(0)
91         , debuggerThreadEngine(0)
92         , guiThreadIsolate(0)
93         , guiThreadEngine(0)
94         , isRunning(true)
95         , internalRequests(0)
96     {
97     }
98
99     ~QV8DebugServicePrivate()
100     {
101         delete debuggerThreadEngine;
102         if (debuggerThreadIsolate)
103             debuggerThreadIsolate->Dispose();
104         delete guiThreadEngine;
105         if (guiThreadIsolate)
106             guiThreadIsolate->Dispose();
107     }
108
109     void initializeDebuggerThread();
110
111     void sendDebugMessage(const QString &message);
112     void updateSourcePath(const QString &sourcePath);
113     static QByteArray packMessage(const QString &message);
114
115     bool connectReceived;
116     bool scheduleBreak;
117
118     v8::Isolate *debuggerThreadIsolate;
119     QJSEngine *debuggerThreadEngine;
120     v8::Isolate *guiThreadIsolate;
121     QJSEngine *guiThreadEngine;
122     // keep messageReceived() from running until initialize() has finished
123     QMutex initializeMutex;
124
125     QList<QDeclarativeEngine *> engines;
126     bool isRunning;
127     QHash<QString, QString> sourcePath;
128     QHash<QString, QString> requestCache;
129     QHash<int, SignalHandlerData> handlersList;
130     int internalRequests;
131 };
132
133 QV8DebugService::QV8DebugService(QObject *parent)
134     : QDeclarativeDebugService(*(new QV8DebugServicePrivate()),
135                                QLatin1String("V8Debugger"), parent)
136 {
137     Q_D(QV8DebugService);
138     v8ServiceInstancePtr = this;
139     // wait for statusChanged() -> initialize()
140     d->initializeMutex.lock();
141     if (registerService() == Enabled) {
142         initialize();
143         // ,block mode, client attached
144         while (!d->connectReceived) {
145             waitForMessage();
146         }
147     } else {
148         d->initializeMutex.unlock();
149     }
150 }
151
152 QV8DebugService::~QV8DebugService()
153 {
154 }
155
156 QV8DebugService *QV8DebugService::instance()
157 {
158     return v8ServiceInstance();
159 }
160
161 void QV8DebugService::addEngine(QDeclarativeEngine *engine)
162 {
163     Q_D(QV8DebugService);
164     Q_ASSERT(engine);
165     Q_ASSERT(!d->engines.contains(engine));
166
167     d->engines.append(engine);
168 }
169
170 void QV8DebugService::removeEngine(QDeclarativeEngine *engine)
171 {
172     Q_D(QV8DebugService);
173     Q_ASSERT(engine);
174     Q_ASSERT(d->engines.contains(engine));
175
176     d->engines.removeAll(engine);
177 }
178
179 void QV8DebugService::debugMessageHandler(const QString &message, bool willStartRunning)
180 {
181     Q_D(QV8DebugService);
182     d->isRunning = willStartRunning;
183
184     if (d->scheduleBreak)
185         scheduledDebugBreak();
186
187     if (d->internalRequests > 0) {
188         // there are outstanding internal requests,
189         // check the sequence number: internal if seq is -1
190
191         QVariantMap responseMap;
192         {
193             v8::Isolate::Scope i_scope(d->guiThreadIsolate);
194             QJSValue parser = d->guiThreadEngine->evaluate(QLatin1String("JSON.parse"));
195             QJSValue out = parser.call(QJSValue(), QJSValueList() << QJSValue(message));
196             responseMap = out.toVariant().toMap();
197         }
198
199         if (responseMap.value(QLatin1String("request_seq")).toInt() == -1) {
200             if (responseMap.value(QLatin1String("command")) == QLatin1String("scripts")) {
201
202                 // Reply to scripts request on connect:
203
204                 // {
205                 //   "type": "response",
206                 //   "request_seq": <number>,
207                 //   "command": "scripts",
208                 //   "body": [ { "name" : <name of script> } ]
209                 // }
210
211                 QVariantList body = responseMap.value(QLatin1String("body")).toList();
212                 foreach (const QVariant &listEntry, body) {
213                     QVariantMap entryMap = listEntry.toMap();
214                     const QString sourcePath = entryMap.value(QLatin1String("name")).toString();
215                     d->updateSourcePath(sourcePath);
216                 }
217             }
218
219             d->internalRequests--;
220             return;
221         }
222     }
223     sendMessage(QV8DebugServicePrivate::packMessage(message));
224 }
225
226
227 void QV8DebugService::appendSourcePath(const QString &message)
228 {
229     Q_D(QV8DebugService);
230
231     QVariantMap msgMap;
232     /* Parse the byte string in a separate isolate
233     This will ensure that the debug message handler does not
234     receive any messages related to this operation */
235     {
236         v8::Isolate::Scope scope(d->guiThreadIsolate);
237         QJSValue parser = d->guiThreadEngine->evaluate(QLatin1String("JSON.parse"));
238         QJSValue out = parser.call(QJSValue(), QJSValueList() << QJSValue(message));
239         msgMap = out.toVariant().toMap();
240     }
241
242     const QString sourcePath(msgMap.value(QLatin1String("body")).toMap().value(
243                                  QLatin1String("script")).toMap().value(
244                                  QLatin1String("name")).toString());
245     d->updateSourcePath(sourcePath);
246 }
247
248 void QV8DebugService::signalEmitted(const QString &signal)
249 {
250     //This function is only called by QDeclarativeBoundSignal
251     //only if there is a slot connected to the signal. Hence, there
252     //is no need for additional check.
253     Q_D(QV8DebugService);
254
255     //Parse just the name and remove the class info
256     //Normalize to Lower case.
257     QString signalName = signal.left(signal.indexOf(QLatin1String("("))).toLower();
258     foreach (const SignalHandlerData &data, d->handlersList) {
259         if (data.functionName == signalName
260                 && data.enabled) {
261             d->scheduleBreak = true;
262         }
263     }
264     if (d->scheduleBreak)
265         scheduledDebugBreak();
266 }
267
268 void QV8DebugService::initialize()
269 {
270     Q_D(QV8DebugService);
271     v8::Debug::SetMessageHandler2(DebugMessageHandler);
272
273     //Create an isolate & engine for parsing JSON messages in GUI thread
274     d->guiThreadIsolate = v8::Isolate::New();
275     v8::Isolate::Scope scope(d->guiThreadIsolate);
276     d->guiThreadEngine = new QJSEngine();
277
278     d->initializeMutex.unlock();
279 }
280
281 void QV8DebugService::scheduledDebugBreak()
282 {
283     Q_D(QV8DebugService);
284     if (d->scheduleBreak) {
285         v8::Debug::DebugBreak();
286         d->scheduleBreak = false;
287     }
288 }
289
290 // executed in the debugger thread
291 void QV8DebugService::statusChanged(QDeclarativeDebugService::Status newStatus)
292 {
293     Q_D(QV8DebugService);
294     if (newStatus == Enabled) {
295         if (!d->debuggerThreadEngine)
296             d->initializeDebuggerThread();
297
298         // execute in GUI thread
299         d->initializeMutex.lock();
300         QMetaObject::invokeMethod(this, "initialize", Qt::QueuedConnection);
301
302         // Request already compiled scripts from v8 (recycling the sequence number from connect)
303
304         // { "seq"       : -1,
305         //    "type"      : "request",
306         //    "command"   : "scripts",
307         //     "arguments" : { "includeSource" : false }
308         // }
309         const QString obj(QLatin1String("{}"));
310         v8::Isolate::Scope i_scope(d->debuggerThreadIsolate);
311         QJSValue parser = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.parse"));
312         QJSValue jsonVal = parser.call(QJSValue(), QJSValueList() << obj);
313         jsonVal.setProperty(QLatin1String("type"), QJSValue(QLatin1String("request")));
314         jsonVal.setProperty(QLatin1String("seq"), QJSValue(-1));
315         jsonVal.setProperty(QLatin1String("command"), QJSValue(QLatin1String("scripts")));
316
317         QJSValue args = parser.call(QJSValue(), QJSValueList() << obj);
318
319         args.setProperty(QLatin1String("includeSource"), QJSValue(false));
320         jsonVal.setProperty(QLatin1String("arguments"), args);
321
322         QJSValue stringify = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.stringify"));
323         QJSValue json = stringify.call(QJSValue(), QJSValueList() << jsonVal);
324         d->internalRequests++;
325         d->sendDebugMessage(json.toString());
326     }
327 }
328
329 // executed in the debugger thread
330 void QV8DebugService::messageReceived(const QByteArray &message)
331 {
332     Q_D(QV8DebugService);
333
334     QDataStream ds(message);
335     QByteArray command;
336     ds >> command;
337
338     QMutexLocker locker(&d->initializeMutex);
339
340     if (command == "V8DEBUG") {
341         if (!d->debuggerThreadEngine)
342             d->initializeDebuggerThread();
343
344
345         QString request;
346         {
347             QByteArray requestArray;
348             ds >> requestArray;
349             request = QString::fromUtf8(requestArray);
350         }
351
352         QVariantMap reqMap;
353         {
354             v8::Isolate::Scope i_scope(d->debuggerThreadIsolate);
355             QJSValue parser = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.parse"));
356             QJSValue out = parser.call(QJSValue(), QJSValueList() << QJSValue(request));
357             reqMap = out.toVariant().toMap();
358         }
359         const QString debugCommand(reqMap.value(QLatin1String("command")).toString());
360         const int sequence = reqMap.value(QLatin1String("seq")).toInt();
361
362         if (debugCommand == QLatin1String("connect")) {
363             d->connectReceived = true;
364             //Prepare the response string
365             //Create a json message using v8 debugging protocol
366             //and send it to client
367
368             // { "type"        : "response",
369             //   "request_seq" : <number>,
370             //   "command"     : "connect",
371             //   "running"     : <is the VM running after sending this response>
372             //   "success"     : true
373             // }
374             {
375                 const QString obj(QLatin1String("{}"));
376                 v8::Isolate::Scope i_scope(d->debuggerThreadIsolate);
377                 QJSValue parser = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.parse"));
378                 QJSValue jsonVal = parser.call(QJSValue(), QJSValueList() << obj);
379                 jsonVal.setProperty(QLatin1String("type"), QJSValue(QLatin1String("response")));
380
381                 jsonVal.setProperty(QLatin1String("request_seq"), QJSValue(sequence));
382                 jsonVal.setProperty(QLatin1String("command"), QJSValue(debugCommand));
383                 jsonVal.setProperty(QLatin1String("success"), QJSValue(true));
384                 jsonVal.setProperty(QLatin1String("running"), QJSValue(d->isRunning));
385
386                 QJSValue stringify = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.stringify"));
387                 QJSValue json = stringify.call(QJSValue(), QJSValueList() << jsonVal);
388                 sendMessage(QV8DebugServicePrivate::packMessage(json.toString()));
389             }
390         } else if (debugCommand == QLatin1String("interrupt")) {
391             //Prepare the response string
392             //Create a json message using v8 debugging protocol
393             //and send it to client
394
395             // { "type"        : "response",
396             //   "request_seq" : <number>,
397             //   "command"     : "interrupt",
398             //   "running"     : <is the VM running after sending this response>
399             //   "success"     : true
400             // }
401             {
402                 const QString obj(QLatin1String("{}"));
403                 v8::Isolate::Scope i_scope(d->debuggerThreadIsolate);
404                 QJSValue parser = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.parse"));
405                 QJSValue jsonVal = parser.call(QJSValue(), QJSValueList() << obj);
406                 jsonVal.setProperty(QLatin1String("type"), QJSValue(QLatin1String("response")));
407
408                 const int sequence = reqMap.value(QLatin1String("seq")).toInt();
409                 jsonVal.setProperty(QLatin1String("request_seq"), QJSValue(sequence));
410                 jsonVal.setProperty(QLatin1String("command"), QJSValue(debugCommand));
411                 jsonVal.setProperty(QLatin1String("success"), QJSValue(true));
412                 jsonVal.setProperty(QLatin1String("running"), QJSValue(d->isRunning));
413
414                 QJSValue stringify = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.stringify"));
415                 QJSValue json = stringify.call(QJSValue(), QJSValueList() << jsonVal);
416                 sendMessage(QV8DebugServicePrivate::packMessage(json.toString()));
417             }
418             // break has to be executed in gui thread
419             d->scheduleBreak = true;
420             QMetaObject::invokeMethod(this, "scheduledDebugBreak", Qt::QueuedConnection);
421         } else {
422             bool forwardRequestToV8 = true;
423
424             if (debugCommand == QLatin1String("setbreakpoint")) {
425                 const QVariantMap arguments = reqMap.value(QLatin1String("arguments")).toMap();
426                 const QString type(arguments.value(QLatin1String("type")).toString());
427
428                 if (type == QLatin1String("script")) {
429                     QString fileName(arguments.value(QLatin1String("target")).toString());
430
431                     //Check if the filepath has been cached
432                     if (d->sourcePath.contains(fileName)) {
433                         QString filePath = d->sourcePath.value(fileName);
434                         request.replace(fileName, filePath);
435                     } else {
436                         //Store the setbreakpoint message till filepath is resolved
437                         d->requestCache.insertMulti(fileName, request);
438                         forwardRequestToV8 = false;
439                     }
440                 } else if (type == QLatin1String("event")) {
441                     //Do not send this request to v8
442                     forwardRequestToV8 = false;
443
444                     //Prepare the response string
445                     //Create a json message using v8 debugging protocol
446                     //and send it to client
447
448                     // { "seq"         : <number>,
449                     //   "type"        : "response",
450                     //   "request_seq" : <number>,
451                     //   "command"     : "setbreakpoint",
452                     //   "body"        : { "type"       : <"function" or "script">
453                     //                     "breakpoint" : <break point number of the new break point>
454                     //                   }
455                     //   "running"     : <is the VM running after sending this response>
456                     //   "success"     : true
457                     // }
458                     {
459                         const QString obj(QLatin1String("{}"));
460                         v8::Isolate::Scope i_scope(d->debuggerThreadIsolate);
461                         QJSValue parser = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.parse"));
462                         QJSValue jsonVal = parser.call(QJSValue(), QJSValueList() << obj);
463                         jsonVal.setProperty(QLatin1String("type"), QJSValue(QLatin1String("response")));
464
465                         const int sequence = reqMap.value(QLatin1String("seq")).toInt();
466                         jsonVal.setProperty(QLatin1String("request_seq"), QJSValue(sequence));
467                         jsonVal.setProperty(QLatin1String("command"), QJSValue(debugCommand));
468
469                         //Check that the function starts with 'on'
470                         QString eventName(arguments.value(QLatin1String("target")).toString());
471
472
473                         if (eventName.startsWith(QLatin1String("on"))) {
474                             SignalHandlerData data;
475                             //Only store the probable signal name.
476                             //Normalize to lower case.
477                             data.functionName = eventName.remove(0,2).toLower();
478                             data.enabled = arguments.value(QLatin1String("enabled")).toBool();
479                             d->handlersList.insert(-sequence, data);
480
481                             QJSValue args = parser.call(QJSValue(), QJSValueList() << obj);
482
483                             args.setProperty(QLatin1String("type"), QJSValue(QLatin1String("event")));
484                             args.setProperty(QLatin1String("breakpoint"), QJSValue(-sequence));
485
486                             jsonVal.setProperty(QLatin1String("body"), args);
487                             jsonVal.setProperty(QLatin1String("success"), QJSValue(true));
488
489                         } else {
490                             jsonVal.setProperty(QLatin1String("success"), QJSValue(false));
491                         }
492
493
494                         jsonVal.setProperty(QLatin1String("running"), QJSValue(d->isRunning));
495
496                         QJSValue stringify = d->debuggerThreadEngine->evaluate(QLatin1String("JSON.stringify"));
497                         QJSValue json = stringify.call(QJSValue(), QJSValueList() << jsonVal);
498                         sendMessage(QV8DebugServicePrivate::packMessage(json.toString()));
499                     }
500                 }
501             } else if (debugCommand == QLatin1String("changebreakpoint")) {
502                 //check if the breakpoint is a negative integer (event breakpoint)
503                 const QVariantMap arguments = reqMap.value(QLatin1String("arguments")).toMap();
504                 const int bp = arguments.value(QLatin1String("breakpoint")).toInt();
505
506                 if (bp < 0) {
507                     SignalHandlerData data = d->handlersList.value(bp);
508                     data.enabled = arguments.value(QLatin1String("enabled")).toBool();
509                     d->handlersList.insert(bp, data);
510                     forwardRequestToV8 = false;
511                 }
512             } else if (debugCommand == QLatin1String("clearbreakpoint")) {
513                 //check if the breakpoint is a negative integer (event breakpoint)
514                 const QVariantMap arguments = reqMap.value(QLatin1String("arguments")).toMap();
515                 const int bp = arguments.value(QLatin1String("breakpoint")).toInt();
516
517                 if (bp < 0) {
518                     d->handlersList.remove(bp);
519                     forwardRequestToV8 = false;
520                 }
521             } else if (debugCommand == QLatin1String("disconnect")) {
522                 v8::Debug::CancelDebugBreak();
523             }
524
525             if (forwardRequestToV8)
526                 d->sendDebugMessage(request);
527         }
528     }
529 }
530
531 void QV8DebugServicePrivate::initializeDebuggerThread()
532 {
533     Q_ASSERT(!debuggerThreadEngine);
534
535     //Create an isolate & engine in debugger thread
536     debuggerThreadIsolate = v8::Isolate::New();
537     v8::Isolate::Scope i_scope(debuggerThreadIsolate);
538     debuggerThreadEngine = new QJSEngine();
539 }
540
541 void QV8DebugServicePrivate::sendDebugMessage(const QString &message)
542 {
543     v8::Debug::SendCommand(message.utf16(), message.size());
544 }
545
546 void QV8DebugServicePrivate::updateSourcePath(const QString &source)
547 {
548     const QString fileName(QFileInfo(source).fileName());
549     sourcePath.insert(fileName, source);
550
551     //Check if there are any pending breakpoint requests for this file
552     if (requestCache.contains(fileName)) {
553         QList<QString> list = requestCache.values(fileName);
554         requestCache.remove(fileName);
555         foreach (QString request, list) {
556             request.replace(fileName, source);
557             sendDebugMessage(request);
558         }
559     }
560 }
561
562 QByteArray QV8DebugServicePrivate::packMessage(const QString &message)
563 {
564     QByteArray reply;
565     QDataStream rs(&reply, QIODevice::WriteOnly);
566     QByteArray cmd("V8DEBUG");
567     rs << cmd << message.toUtf8();
568     return reply;
569 }
570
571 QT_END_NAMESPACE