DebuggerAgent.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. // node-inspector version of on webkit-inspector/DebuggerAgent.cpp
  2. var convert = require('./convert.js'),
  3. format = require('util').format,
  4. path = require('path'),
  5. async = require('async'),
  6. ScriptFileStorage = require('./ScriptFileStorage').ScriptFileStorage;
  7. /**
  8. * @param {{saveLiveEdit,preload}} config
  9. * @param {FrontendClient} frontendClient
  10. * @param {DebuggerClient} debuggerClient
  11. * @param {BreakEventHandler} breakEventHandler
  12. * @param {ScriptManager} scriptManager
  13. * @constructor
  14. */
  15. function DebuggerAgent(config,
  16. frontendClient,
  17. debuggerClient,
  18. breakEventHandler,
  19. scriptManager) {
  20. this._saveLiveEdit = config.saveLiveEdit;
  21. this._frontendClient = frontendClient;
  22. this._debuggerClient = debuggerClient;
  23. this._breakEventHandler = breakEventHandler;
  24. this._scriptManager = scriptManager;
  25. this._scriptStorage = new ScriptFileStorage(config, scriptManager);
  26. }
  27. DebuggerAgent.prototype = {
  28. canSetScriptSource: function(params, done) {
  29. done(null, { result: true });
  30. },
  31. enable: function(params, done) {
  32. var onConnect = function() {
  33. done();
  34. this._onDebuggerConnect();
  35. }.bind(this);
  36. if (this._debuggerClient.isConnected) {
  37. process.nextTick(onConnect);
  38. } else {
  39. this._debuggerClient.on('connect', onConnect);
  40. this._debuggerClient.connect();
  41. }
  42. },
  43. _onDebuggerConnect: function() {
  44. async.waterfall([
  45. // Remove all existing breakpoints because:
  46. // 1) front-end inspector cannot restore breakpoints from debugger anyway
  47. // 2) all breakpoints were disabled when the previous debugger-client
  48. // disconnected from the debugged application
  49. this._removeAllBreakpoints.bind(this),
  50. this._reloadScripts.bind(this),
  51. this._sendBacktraceIfPaused.bind(this)
  52. ]);
  53. },
  54. _removeAllBreakpoints: function(done) {
  55. this._debuggerClient.request(
  56. 'listbreakpoints',
  57. {},
  58. function(err, response) {
  59. if (err) {
  60. console.log('Warning: cannot remove old breakpoints. %s', err);
  61. done();
  62. return;
  63. }
  64. function removeOneBreakpoint(bp, next) {
  65. this._debuggerClient.clearBreakpoint(
  66. bp.number,
  67. function(error) {
  68. if (error)
  69. console.log(
  70. 'Warning: cannot remove old breakpoint %d. %s',
  71. bp.number,
  72. error
  73. );
  74. next();
  75. }
  76. );
  77. }
  78. async.eachSeries(
  79. response.breakpoints,
  80. removeOneBreakpoint.bind(this),
  81. done
  82. );
  83. }.bind(this)
  84. );
  85. },
  86. _reloadScripts: function(done) {
  87. this._scriptManager.reset();
  88. this._debuggerClient.request(
  89. 'scripts',
  90. {
  91. includeSource: true,
  92. types: 4
  93. },
  94. function handleScriptsResponse(err, result) {
  95. if (err) {
  96. done(err);
  97. return;
  98. }
  99. result.forEach(this._scriptManager.addScript.bind(this._scriptManager));
  100. done();
  101. }.bind(this)
  102. );
  103. },
  104. _sendBacktraceIfPaused: function(done) {
  105. if (!this._debuggerClient.isRunning) {
  106. this._breakEventHandler.sendBacktraceToFrontend(null);
  107. }
  108. done();
  109. },
  110. disable: function(params, done) {
  111. this._debuggerClient.close();
  112. done();
  113. },
  114. resume: function(params, done) {
  115. this._sendContinue(undefined, done);
  116. },
  117. _sendContinue: function(stepAction, done) {
  118. var args = stepAction ? { stepaction: stepAction } : undefined;
  119. this._debuggerClient.request('continue', args, function(error, result) {
  120. done(error);
  121. if (!error)
  122. this._frontendClient.sendEvent('Debugger.resumed');
  123. }.bind(this));
  124. },
  125. pause: function(params, done) {
  126. this._debuggerClient.request('suspend', {}, function(error, result) {
  127. done(error);
  128. if (!error) {
  129. this._breakEventHandler.sendBacktraceToFrontend(null);
  130. }
  131. }.bind(this));
  132. },
  133. stepOver: function(params, done) {
  134. this._sendContinue('next', done);
  135. },
  136. stepInto: function(params, done) {
  137. this._sendContinue('in', done);
  138. },
  139. stepOut: function(params, done) {
  140. this._sendContinue('out', done);
  141. },
  142. continueToLocation: function(params, done) {
  143. var requestParams = {
  144. type: 'scriptId',
  145. target: convert.inspectorScriptIdToV8Id(params.location.scriptId),
  146. line: params.location.lineNumber,
  147. column: params.location.columnNumber
  148. };
  149. this._debuggerClient.request('setbreakpoint', requestParams, function(error, response) {
  150. if (error != null) {
  151. done(error);
  152. return;
  153. }
  154. this._breakEventHandler.
  155. continueToLocationBreakpointId = response.breakpoint;
  156. this._debuggerClient.request('continue', undefined, function(error, response) {
  157. done(error);
  158. });
  159. }.bind(this));
  160. },
  161. getScriptSource: function(params, done) {
  162. this._debuggerClient.getScriptSourceById(
  163. Number(params.scriptId),
  164. function(err, source) {
  165. if (err) return done(err);
  166. return done(null, { scriptSource: source });
  167. }
  168. );
  169. },
  170. setScriptSource: function(params, done) {
  171. this._debuggerClient.request(
  172. 'changelive',
  173. {
  174. script_id: convert.inspectorScriptIdToV8Id(params.scriptId),
  175. new_source: params.scriptSource,
  176. preview_only: false
  177. },
  178. function(err, response) {
  179. this._handleChangeLiveOrRestartFrameResponse(done, err, response);
  180. this._persistScriptChanges(params.scriptId, params.scriptSource);
  181. }.bind(this)
  182. );
  183. },
  184. _handleChangeLiveOrRestartFrameResponse: function(done, err, response) {
  185. if (err) {
  186. done(err);
  187. return;
  188. }
  189. var frontendClient = this._frontendClient;
  190. var breakEventHandler = this._breakEventHandler;
  191. function sendResponse(callframes) {
  192. done(
  193. null,
  194. {
  195. callFrames: callframes || [],
  196. result: response.result
  197. }
  198. );
  199. }
  200. function sendResponseWithCallStack() {
  201. breakEventHandler.fetchCallFrames(function(err, response) {
  202. var callframes = [];
  203. if (err) {
  204. frontendClient.sendLogToConsole(
  205. 'error',
  206. 'Cannot update stack trace after a script changed: ' + err);
  207. } else {
  208. callframes = response;
  209. }
  210. sendResponse(callframes);
  211. });
  212. }
  213. var result = response.result;
  214. if (result.stack_modified && !result.stack_update_needs_step_in)
  215. sendResponseWithCallStack();
  216. else
  217. sendResponse();
  218. },
  219. _persistScriptChanges: function(scriptId, newSource) {
  220. if (!this._saveLiveEdit) {
  221. this._warn(
  222. 'Saving of live-edit changes back to source files is disabled by configuration.\n' +
  223. 'Change the option "saveLiveEdit" in config.json to enable this feature.'
  224. );
  225. return;
  226. }
  227. var source = this._scriptManager.findScriptByID(scriptId);
  228. if (!source) {
  229. this._warn('Cannot save changes to disk: unknown script id %s', scriptId);
  230. return;
  231. }
  232. var scriptFile = source.v8name;
  233. if (!scriptFile || scriptFile.indexOf(path.sep) == -1) {
  234. this._warn(
  235. 'Cannot save changes to disk: script id %s "%s" was not loaded from a file.',
  236. scriptId,
  237. scriptFile || 'null'
  238. );
  239. return;
  240. }
  241. this._scriptStorage.save(scriptFile, newSource, function(err) {
  242. if (err) {
  243. this._warn('Cannot save changes to disk. %s', err);
  244. }
  245. }.bind(this));
  246. },
  247. _warn: function() {
  248. this._frontendClient.sendLogToConsole(
  249. 'warning',
  250. format.apply(this, arguments)
  251. );
  252. },
  253. setPauseOnExceptions: function(params, done) {
  254. var args = [
  255. { type: 'all', enabled: params.state == 'all' },
  256. { type: 'uncaught', enabled: params.state == 'uncaught' }
  257. ];
  258. async.eachSeries(
  259. args,
  260. function(arg, next) {
  261. this._debuggerClient.request('setexceptionbreak', arg, next);
  262. }.bind(this),
  263. done);
  264. },
  265. setBreakpointByUrl: function(params, done) {
  266. if (params.urlRegex !== undefined) {
  267. // DevTools protocol defines urlRegex parameter,
  268. // but the parameter is not used by the front-end.
  269. done('Error: setBreakpointByUrl using urlRegex is not implemented.');
  270. return;
  271. }
  272. var requestParams = {
  273. type: 'script',
  274. target: convert.inspectorUrlToV8Name(params.url),
  275. line: params.lineNumber,
  276. column: params.columnNumber,
  277. condition: params.condition
  278. };
  279. this._debuggerClient.request('setbreakpoint', requestParams, function(error, response) {
  280. if (error != null) {
  281. done(error);
  282. return;
  283. }
  284. done(null, {
  285. breakpointId: response.breakpoint.toString(),
  286. locations: response.actual_locations.map(convert.v8LocationToInspectorLocation)
  287. });
  288. });
  289. },
  290. removeBreakpoint: function(params, done) {
  291. this._debuggerClient.clearBreakpoint(
  292. params.breakpointId,
  293. function(error, response) {
  294. done(error, null);
  295. }
  296. );
  297. },
  298. setBreakpointsActive: function(params, done) {
  299. this._debuggerClient.request('listbreakpoints', {}, function(error, response) {
  300. if (error) {
  301. done(error);
  302. return;
  303. }
  304. function setBreakpointState(bp, next) {
  305. var req = { breakpoint: bp.number, enabled: params.active };
  306. this._debuggerClient.request('changebreakpoint', req, next);
  307. }
  308. async.eachSeries(response.breakpoints, setBreakpointState.bind(this), done);
  309. }.bind(this));
  310. },
  311. setOverlayMessage: function(params, done) {
  312. done();
  313. },
  314. evaluateOnCallFrame: function(params, done) {
  315. var self = this;
  316. var expression = params.expression;
  317. var frame = Number(params.callFrameId);
  318. self._debuggerClient.request(
  319. 'evaluate',
  320. {
  321. expression: params.expression,
  322. frame: frame
  323. },
  324. function(err, result) {
  325. // Errors from V8 are actually just messages, so we need to fill them out a bit.
  326. if (err) {
  327. err = convert.v8ErrorToInspectorError(err);
  328. }
  329. done(null, {
  330. result: err || convert.v8ResultToInspectorResult(result),
  331. wasThrown: !!err
  332. });
  333. }
  334. );
  335. },
  336. getFunctionDetails: function(params, done) {
  337. var handle = params.functionId;
  338. this._debuggerClient.request(
  339. 'lookup',
  340. {
  341. handles: [handle],
  342. includeSource: false
  343. },
  344. function(error, responseBody) {
  345. if (error) {
  346. done(error);
  347. } else {
  348. done(null, convert.v8FunctionLookupToFunctionDetails(responseBody[handle]));
  349. }
  350. }.bind(this));
  351. },
  352. restartFrame: function(params, done) {
  353. this._debuggerClient.request(
  354. 'restartframe',
  355. {
  356. frame: Number(params.callFrameId)
  357. },
  358. this._handleChangeLiveOrRestartFrameResponse.bind(this, done)
  359. );
  360. },
  361. setVariableValue: function(params, done) {
  362. this._debuggerClient.evaluateGlobal('process.version', function(err, version) {
  363. if (!DebuggerAgent.nodeVersionHasSetVariableValue(version)) {
  364. done(
  365. 'V8 engine in node version ' + version +
  366. ' does not support setting variable value from debugger.\n' +
  367. ' Please upgrade to version v0.10.12 (stable) or v0.11.2 (unstable)' +
  368. ' or newer.');
  369. } else {
  370. this._doSetVariableValue(params, done);
  371. }
  372. }.bind(this));
  373. },
  374. _doSetVariableValue: function(params, done) {
  375. var value = convert.inspectorValueToV8Value(params.newValue);
  376. this._debuggerClient.request(
  377. 'setVariableValue',
  378. {
  379. name: params.variableName,
  380. scope: {
  381. number: Number(params.scopeNumber),
  382. frameNumber: Number(params.callFrameId)
  383. },
  384. newValue: value
  385. },
  386. function(err, result) {
  387. done(err, result);
  388. }
  389. );
  390. },
  391. setSkipAllPauses: function(params, done) {
  392. if (params.skipped)
  393. done(new Error('Not implemented.'));
  394. else
  395. done();
  396. }
  397. };
  398. DebuggerAgent.nodeVersionHasSetVariableValue = function(version) {
  399. var match = /^v(\d+)\.(\d+)\.(\d+)$/.exec(version);
  400. if (!match) return false;
  401. return match[1] > 0 || // v1+
  402. (match[2] == 10 && match[3] >= 12) || // v0.10.12+
  403. (match[2] == 11 && match[3] >= 2) || // v0.11.2+
  404. (match[2] >= 12); // v0.12+
  405. };
  406. exports.DebuggerAgent = DebuggerAgent;