DebuggerClient.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. var extend = require('util')._extend;
  2. var EventEmitter = require('events').EventEmitter,
  3. inherits = require('util').inherits,
  4. DebugConnection = require('./debugger.js');
  5. function createFailingConnection(reason) {
  6. return {
  7. connected: false,
  8. isRunning: false,
  9. request: function(command, args, callback) {
  10. callback({ message: new ErrorNotConnected(reason) });
  11. },
  12. close: function() {
  13. }
  14. };
  15. }
  16. /**
  17. * @constructor
  18. * @param {number} debuggerPort
  19. */
  20. function DebuggerClient(debuggerPort) {
  21. this._conn = createFailingConnection('node-inspector server was restarted');
  22. this._port = debuggerPort;
  23. }
  24. inherits(DebuggerClient, EventEmitter);
  25. Object.defineProperties(DebuggerClient.prototype, {
  26. /** @type {boolean} */
  27. isRunning: {
  28. get: function() {
  29. return this._conn.isRunning;
  30. }
  31. },
  32. isConnected: {
  33. get: function() {
  34. return this._conn.connected;
  35. }
  36. }
  37. });
  38. DebuggerClient.prototype.connect = function() {
  39. this._conn = DebugConnection.attachDebugger(this._port);
  40. this._conn.
  41. on('connect', this._onConnectionOpen.bind(this)).
  42. on('error', this.emit.bind(this, 'error')).
  43. on('close', this._onConnectionClose.bind(this));
  44. this._registerDebuggerEventHandlers('break', 'afterCompile', 'exception');
  45. };
  46. /**
  47. * @param {...string} eventNames
  48. */
  49. DebuggerClient.prototype._registerDebuggerEventHandlers = function(eventNames) {
  50. for (var i in arguments) {
  51. var name = arguments[i];
  52. this._conn.on(name, this._emitDebuggerEvent.bind(this, name));
  53. }
  54. };
  55. DebuggerClient.prototype._onConnectionOpen = function() {
  56. //We need to update isRunning state before we continue with debugging.
  57. //Send the dummy requestso that we can read the state from the response.
  58. this.request('version', {}, function(error, result) {
  59. this.emit('connect');
  60. }.bind(this));
  61. };
  62. /**
  63. * @param {string} reason
  64. */
  65. DebuggerClient.prototype._onConnectionClose = function(reason) {
  66. this._conn = createFailingConnection(reason);
  67. this.emit('close', reason);
  68. };
  69. /**
  70. * @param {string} name
  71. * @param {Object} message
  72. */
  73. DebuggerClient.prototype._emitDebuggerEvent = function(name, message) {
  74. this.emit(name, message.body);
  75. };
  76. /**
  77. * @param {string} command
  78. * @param {!Object} args
  79. * @param {function(error, response, refs)} callback
  80. */
  81. DebuggerClient.prototype.request = function(command, args, callback) {
  82. if (typeof callback !== 'function') {
  83. callback = function(error) {
  84. if (!error) return;
  85. console.log('Warning: ignored V8 debugger error. %s', error);
  86. };
  87. }
  88. // Note: we must not add args object if it was not sent.
  89. // E.g. resume (V8 request 'continue') does no work
  90. // correctly when args are empty instead of undefined
  91. if (args && args.maxStringLength == null)
  92. args.maxStringLength = 10000;
  93. this._conn.request(command, { arguments: args }, function(response) {
  94. var refsLookup;
  95. if (!response.success)
  96. callback(response.message);
  97. else {
  98. refsLookup = {};
  99. if (response.refs)
  100. response.refs.forEach(function(r) { refsLookup[r.handle] = r; });
  101. callback(null, response.body, refsLookup);
  102. }
  103. });
  104. };
  105. /**
  106. */
  107. DebuggerClient.prototype.close = function() {
  108. this._conn.close();
  109. };
  110. /**
  111. * @param {number} breakpointId
  112. * @param {function(error, response, refs)} done
  113. */
  114. DebuggerClient.prototype.clearBreakpoint = function(breakpointId, done) {
  115. this.request(
  116. 'clearbreakpoint',
  117. {
  118. breakpoint: breakpointId
  119. },
  120. done
  121. );
  122. };
  123. /**
  124. * @param {string} expression
  125. * @param {function(error, response)} done
  126. */
  127. DebuggerClient.prototype.evaluateGlobal = function(expression, done) {
  128. // Note: we can't simply evaluate JSON.stringify(`expression`)
  129. // because V8 debugger protocol truncates returned value to 80 characters
  130. // The workaround is to split the serialized value into multiple pieces,
  131. // each piece 80 characters long, send an array over the wire,
  132. // and reconstruct the value back here
  133. var code = 'JSON.stringify(' + expression + ').match(/.{1,80}/g).slice()';
  134. this.request(
  135. 'evaluate',
  136. {
  137. expression: code,
  138. global: true
  139. },
  140. function _handleEvaluateResponse(err, result, refs) {
  141. if (err) return done(err);
  142. if (result.type != 'object' && result.className != 'Array') {
  143. return done(
  144. new Error(
  145. 'Evaluate returned unexpected result:' +
  146. ' type: ' + result.type +
  147. ' className: ' + result.className
  148. )
  149. );
  150. }
  151. var fullJsonString = result.properties
  152. .filter(function isArrayIndex(p) { return /^\d+$/.test(p.name);})
  153. .map(function resolvePropertyValue(p) { return refs[p.ref].value; })
  154. .join('');
  155. try {
  156. done(null, JSON.parse(fullJsonString));
  157. } catch (e) {
  158. console.error('evaluateGlobal "%s" failed', expression);
  159. console.error(e.stack);
  160. console.error('--json-begin--\n%s--json-end--', fullJsonString);
  161. done(e);
  162. }
  163. }
  164. );
  165. };
  166. /**
  167. * @param {number} id
  168. * @param {function(Object, string?)} callback
  169. */
  170. DebuggerClient.prototype.getScriptSourceById = function(id, callback) {
  171. this.request(
  172. 'scripts',
  173. {
  174. includeSource: true,
  175. types: 4,
  176. ids: [id]
  177. },
  178. function handleScriptSourceResponse(err, result) {
  179. if (err) return callback(err);
  180. // Some modules gets unloaded (?) after they are parsed,
  181. // e.g. node_modules/express/node_modules/methods/index.js
  182. // V8 request 'scripts' returns an empty result in such case
  183. var source = result.length > 0 ? result[0].source : undefined;
  184. callback(null, source);
  185. }
  186. );
  187. };
  188. /**
  189. * @param {string} message
  190. * @constructor
  191. */
  192. function ErrorNotConnected(message) {
  193. Error.call(this);
  194. this.name = ErrorNotConnected.name;
  195. this.message = message;
  196. }
  197. inherits(ErrorNotConnected, Error);
  198. exports.DebuggerClient = DebuggerClient;
  199. exports.ErrorNotConnected = ErrorNotConnected;