debugger-commands.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const { Cc, Ci, Cu } = require("chrome");
  7. const l10n = require("gcli/l10n");
  8. loader.lazyRequireGetter(this, "gDevTools",
  9. "devtools/client/framework/devtools", true);
  10. /**
  11. * The commands and converters that are exported to GCLI
  12. */
  13. exports.items = [];
  14. /**
  15. * Utility to get access to the current breakpoint list.
  16. *
  17. * @param DebuggerPanel dbg
  18. * The debugger panel.
  19. * @return array
  20. * An array of objects, one for each breakpoint, where each breakpoint
  21. * object has the following properties:
  22. * - url: the URL of the source file.
  23. * - label: a unique string identifier designed to be user visible.
  24. * - lineNumber: the line number of the breakpoint in the source file.
  25. * - lineText: the text of the line at the breakpoint.
  26. * - truncatedLineText: lineText truncated to MAX_LINE_TEXT_LENGTH.
  27. */
  28. function getAllBreakpoints(dbg) {
  29. let breakpoints = [];
  30. let sources = dbg._view.Sources;
  31. let { trimUrlLength: trim } = dbg.panelWin.SourceUtils;
  32. for (let source of sources) {
  33. for (let { attachment: breakpoint } of source) {
  34. breakpoints.push({
  35. url: source.attachment.source.url,
  36. label: source.attachment.label + ":" + breakpoint.line,
  37. lineNumber: breakpoint.line,
  38. lineText: breakpoint.text,
  39. truncatedLineText: trim(breakpoint.text, MAX_LINE_TEXT_LENGTH, "end")
  40. });
  41. }
  42. }
  43. return breakpoints;
  44. }
  45. function getAllSources(dbg) {
  46. if (!dbg) {
  47. return [];
  48. }
  49. let items = dbg._view.Sources.items;
  50. return items
  51. .filter(item => !!item.attachment.source.url)
  52. .map(item => ({
  53. name: item.attachment.source.url,
  54. value: item.attachment.source.actor
  55. }));
  56. }
  57. /**
  58. * 'break' command
  59. */
  60. exports.items.push({
  61. name: "break",
  62. description: l10n.lookup("breakDesc"),
  63. manual: l10n.lookup("breakManual")
  64. });
  65. /**
  66. * 'break list' command
  67. */
  68. exports.items.push({
  69. name: "break list",
  70. item: "command",
  71. runAt: "client",
  72. description: l10n.lookup("breaklistDesc"),
  73. returnType: "breakpoints",
  74. exec: function (args, context) {
  75. let dbg = getPanel(context, "jsdebugger", { ensureOpened: true });
  76. return dbg.then(getAllBreakpoints);
  77. }
  78. });
  79. exports.items.push({
  80. item: "converter",
  81. from: "breakpoints",
  82. to: "view",
  83. exec: function (breakpoints, context) {
  84. let dbg = getPanel(context, "jsdebugger");
  85. if (dbg && breakpoints.length) {
  86. return context.createView({
  87. html: breakListHtml,
  88. data: {
  89. breakpoints: breakpoints,
  90. onclick: context.update,
  91. ondblclick: context.updateExec
  92. }
  93. });
  94. } else {
  95. return context.createView({
  96. html: "<p>${message}</p>",
  97. data: { message: l10n.lookup("breaklistNone") }
  98. });
  99. }
  100. }
  101. });
  102. var breakListHtml = "" +
  103. "<table>" +
  104. " <thead>" +
  105. " <th>Source</th>" +
  106. " <th>Line</th>" +
  107. " <th>Actions</th>" +
  108. " </thead>" +
  109. " <tbody>" +
  110. " <tr foreach='breakpoint in ${breakpoints}'>" +
  111. " <td class='gcli-breakpoint-label'>${breakpoint.label}</td>" +
  112. " <td class='gcli-breakpoint-lineText'>" +
  113. " ${breakpoint.truncatedLineText}" +
  114. " </td>" +
  115. " <td>" +
  116. " <span class='gcli-out-shortcut'" +
  117. " data-command='break del ${breakpoint.label}'" +
  118. " onclick='${onclick}'" +
  119. " ondblclick='${ondblclick}'>" +
  120. " " + l10n.lookup("breaklistOutRemove") + "</span>" +
  121. " </td>" +
  122. " </tr>" +
  123. " </tbody>" +
  124. "</table>" +
  125. "";
  126. var MAX_LINE_TEXT_LENGTH = 30;
  127. var MAX_LABEL_LENGTH = 20;
  128. /**
  129. * 'break add' command
  130. */
  131. exports.items.push({
  132. name: "break add",
  133. description: l10n.lookup("breakaddDesc"),
  134. manual: l10n.lookup("breakaddManual")
  135. });
  136. /**
  137. * 'break add line' command
  138. */
  139. exports.items.push({
  140. item: "command",
  141. runAt: "client",
  142. name: "break add line",
  143. description: l10n.lookup("breakaddlineDesc"),
  144. params: [
  145. {
  146. name: "file",
  147. type: {
  148. name: "selection",
  149. lookup: function (context) {
  150. return getAllSources(getPanel(context, "jsdebugger"));
  151. }
  152. },
  153. description: l10n.lookup("breakaddlineFileDesc")
  154. },
  155. {
  156. name: "line",
  157. type: { name: "number", min: 1, step: 10 },
  158. description: l10n.lookup("breakaddlineLineDesc")
  159. }
  160. ],
  161. returnType: "string",
  162. exec: function (args, context) {
  163. let dbg = getPanel(context, "jsdebugger");
  164. if (!dbg) {
  165. return l10n.lookup("debuggerStopped");
  166. }
  167. let deferred = context.defer();
  168. let item = dbg._view.Sources.getItemForAttachment(a => {
  169. return a.source && a.source.actor === args.file;
  170. });
  171. let position = { actor: item.value, line: args.line };
  172. dbg.addBreakpoint(position).then(() => {
  173. deferred.resolve(l10n.lookup("breakaddAdded"));
  174. }, aError => {
  175. deferred.resolve(l10n.lookupFormat("breakaddFailed", [aError]));
  176. });
  177. return deferred.promise;
  178. }
  179. });
  180. /**
  181. * 'break del' command
  182. */
  183. exports.items.push({
  184. item: "command",
  185. runAt: "client",
  186. name: "break del",
  187. description: l10n.lookup("breakdelDesc"),
  188. params: [
  189. {
  190. name: "breakpoint",
  191. type: {
  192. name: "selection",
  193. lookup: function (context) {
  194. let dbg = getPanel(context, "jsdebugger");
  195. if (!dbg) {
  196. return [];
  197. }
  198. return getAllBreakpoints(dbg).map(breakpoint => ({
  199. name: breakpoint.label,
  200. value: breakpoint,
  201. description: breakpoint.truncatedLineText
  202. }));
  203. }
  204. },
  205. description: l10n.lookup("breakdelBreakidDesc")
  206. }
  207. ],
  208. returnType: "string",
  209. exec: function (args, context) {
  210. let dbg = getPanel(context, "jsdebugger");
  211. if (!dbg) {
  212. return l10n.lookup("debuggerStopped");
  213. }
  214. let source = dbg._view.Sources.getItemForAttachment(a => {
  215. return a.source && a.source.url === args.breakpoint.url;
  216. });
  217. let deferred = context.defer();
  218. let position = { actor: source.attachment.source.actor,
  219. line: args.breakpoint.lineNumber };
  220. dbg.removeBreakpoint(position).then(() => {
  221. deferred.resolve(l10n.lookup("breakdelRemoved"));
  222. }, () => {
  223. deferred.resolve(l10n.lookup("breakNotFound"));
  224. });
  225. return deferred.promise;
  226. }
  227. });
  228. /**
  229. * 'dbg' command
  230. */
  231. exports.items.push({
  232. name: "dbg",
  233. description: l10n.lookup("dbgDesc"),
  234. manual: l10n.lookup("dbgManual")
  235. });
  236. /**
  237. * 'dbg open' command
  238. */
  239. exports.items.push({
  240. item: "command",
  241. runAt: "client",
  242. name: "dbg open",
  243. description: l10n.lookup("dbgOpen"),
  244. params: [],
  245. exec: function (args, context) {
  246. let target = context.environment.target;
  247. return gDevTools.showToolbox(target, "jsdebugger").then(() => null);
  248. }
  249. });
  250. /**
  251. * 'dbg close' command
  252. */
  253. exports.items.push({
  254. item: "command",
  255. runAt: "client",
  256. name: "dbg close",
  257. description: l10n.lookup("dbgClose"),
  258. params: [],
  259. exec: function (args, context) {
  260. if (!getPanel(context, "jsdebugger")) {
  261. return;
  262. }
  263. let target = context.environment.target;
  264. return gDevTools.closeToolbox(target).then(() => null);
  265. }
  266. });
  267. /**
  268. * 'dbg interrupt' command
  269. */
  270. exports.items.push({
  271. item: "command",
  272. runAt: "client",
  273. name: "dbg interrupt",
  274. description: l10n.lookup("dbgInterrupt"),
  275. params: [],
  276. exec: function (args, context) {
  277. let dbg = getPanel(context, "jsdebugger");
  278. if (!dbg) {
  279. return l10n.lookup("debuggerStopped");
  280. }
  281. let controller = dbg._controller;
  282. let thread = controller.activeThread;
  283. if (!thread.paused) {
  284. thread.interrupt();
  285. }
  286. }
  287. });
  288. /**
  289. * 'dbg continue' command
  290. */
  291. exports.items.push({
  292. item: "command",
  293. runAt: "client",
  294. name: "dbg continue",
  295. description: l10n.lookup("dbgContinue"),
  296. params: [],
  297. exec: function (args, context) {
  298. let dbg = getPanel(context, "jsdebugger");
  299. if (!dbg) {
  300. return l10n.lookup("debuggerStopped");
  301. }
  302. let controller = dbg._controller;
  303. let thread = controller.activeThread;
  304. if (thread.paused) {
  305. thread.resume();
  306. }
  307. }
  308. });
  309. /**
  310. * 'dbg step' command
  311. */
  312. exports.items.push({
  313. item: "command",
  314. runAt: "client",
  315. name: "dbg step",
  316. description: l10n.lookup("dbgStepDesc"),
  317. manual: l10n.lookup("dbgStepManual")
  318. });
  319. /**
  320. * 'dbg step over' command
  321. */
  322. exports.items.push({
  323. item: "command",
  324. runAt: "client",
  325. name: "dbg step over",
  326. description: l10n.lookup("dbgStepOverDesc"),
  327. params: [],
  328. exec: function (args, context) {
  329. let dbg = getPanel(context, "jsdebugger");
  330. if (!dbg) {
  331. return l10n.lookup("debuggerStopped");
  332. }
  333. let controller = dbg._controller;
  334. let thread = controller.activeThread;
  335. if (thread.paused) {
  336. thread.stepOver();
  337. }
  338. }
  339. });
  340. /**
  341. * 'dbg step in' command
  342. */
  343. exports.items.push({
  344. item: "command",
  345. runAt: "client",
  346. name: "dbg step in",
  347. description: l10n.lookup("dbgStepInDesc"),
  348. params: [],
  349. exec: function (args, context) {
  350. let dbg = getPanel(context, "jsdebugger");
  351. if (!dbg) {
  352. return l10n.lookup("debuggerStopped");
  353. }
  354. let controller = dbg._controller;
  355. let thread = controller.activeThread;
  356. if (thread.paused) {
  357. thread.stepIn();
  358. }
  359. }
  360. });
  361. /**
  362. * 'dbg step over' command
  363. */
  364. exports.items.push({
  365. item: "command",
  366. runAt: "client",
  367. name: "dbg step out",
  368. description: l10n.lookup("dbgStepOutDesc"),
  369. params: [],
  370. exec: function (args, context) {
  371. let dbg = getPanel(context, "jsdebugger");
  372. if (!dbg) {
  373. return l10n.lookup("debuggerStopped");
  374. }
  375. let controller = dbg._controller;
  376. let thread = controller.activeThread;
  377. if (thread.paused) {
  378. thread.stepOut();
  379. }
  380. }
  381. });
  382. /**
  383. * 'dbg list' command
  384. */
  385. exports.items.push({
  386. item: "command",
  387. runAt: "client",
  388. name: "dbg list",
  389. description: l10n.lookup("dbgListSourcesDesc"),
  390. params: [],
  391. returnType: "dom",
  392. exec: function (args, context) {
  393. let dbg = getPanel(context, "jsdebugger");
  394. if (!dbg) {
  395. return l10n.lookup("debuggerClosed");
  396. }
  397. let sources = getAllSources(dbg);
  398. let doc = context.environment.chromeDocument;
  399. let div = createXHTMLElement(doc, "div");
  400. let ol = createXHTMLElement(doc, "ol");
  401. sources.forEach(source => {
  402. let li = createXHTMLElement(doc, "li");
  403. li.textContent = source.name;
  404. ol.appendChild(li);
  405. });
  406. div.appendChild(ol);
  407. return div;
  408. }
  409. });
  410. /**
  411. * Define the 'dbg blackbox' and 'dbg unblackbox' commands.
  412. */
  413. [
  414. {
  415. name: "blackbox",
  416. clientMethod: "blackBox",
  417. l10nPrefix: "dbgBlackBox"
  418. },
  419. {
  420. name: "unblackbox",
  421. clientMethod: "unblackBox",
  422. l10nPrefix: "dbgUnBlackBox"
  423. }
  424. ].forEach(function (cmd) {
  425. const lookup = function (id) {
  426. return l10n.lookup(cmd.l10nPrefix + id);
  427. };
  428. exports.items.push({
  429. item: "command",
  430. runAt: "client",
  431. name: "dbg " + cmd.name,
  432. description: lookup("Desc"),
  433. params: [
  434. {
  435. name: "source",
  436. type: {
  437. name: "selection",
  438. lookup: function (context) {
  439. return getAllSources(getPanel(context, "jsdebugger"));
  440. }
  441. },
  442. description: lookup("SourceDesc"),
  443. defaultValue: null
  444. },
  445. {
  446. name: "glob",
  447. type: "string",
  448. description: lookup("GlobDesc"),
  449. defaultValue: null
  450. },
  451. {
  452. name: "invert",
  453. type: "boolean",
  454. description: lookup("InvertDesc")
  455. }
  456. ],
  457. returnType: "dom",
  458. exec: function (args, context) {
  459. const dbg = getPanel(context, "jsdebugger");
  460. const doc = context.environment.chromeDocument;
  461. if (!dbg) {
  462. throw new Error(l10n.lookup("debuggerClosed"));
  463. }
  464. const { promise, resolve, reject } = context.defer();
  465. const { activeThread } = dbg._controller;
  466. const globRegExp = args.glob ? globToRegExp(args.glob) : null;
  467. // Filter the sources down to those that we will need to black box.
  468. function shouldBlackBox(source) {
  469. var value = globRegExp && globRegExp.test(source.url)
  470. || args.source && source.actor == args.source;
  471. return args.invert ? !value : value;
  472. }
  473. const toBlackBox = [];
  474. for (let {attachment: {source}} of dbg._view.Sources.items) {
  475. if (shouldBlackBox(source)) {
  476. toBlackBox.push(source);
  477. }
  478. }
  479. // If we aren't black boxing any sources, bail out now.
  480. if (toBlackBox.length === 0) {
  481. const empty = createXHTMLElement(doc, "div");
  482. empty.textContent = lookup("EmptyDesc");
  483. return void resolve(empty);
  484. }
  485. // Send the black box request to each source we are black boxing. As we
  486. // get responses, accumulate the results in `blackBoxed`.
  487. const blackBoxed = [];
  488. for (let source of toBlackBox) {
  489. dbg.blackbox(source, cmd.clientMethod === "blackBox").then(() => {
  490. blackBoxed.push(source.url);
  491. }, err => {
  492. blackBoxed.push(lookup("ErrorDesc") + " " + source.url);
  493. }).then(() => {
  494. if (toBlackBox.length === blackBoxed.length) {
  495. displayResults();
  496. }
  497. });
  498. }
  499. // List the results for the user.
  500. function displayResults() {
  501. const results = doc.createElement("div");
  502. results.textContent = lookup("NonEmptyDesc");
  503. const list = createXHTMLElement(doc, "ul");
  504. results.appendChild(list);
  505. for (let result of blackBoxed) {
  506. const item = createXHTMLElement(doc, "li");
  507. item.textContent = result;
  508. list.appendChild(item);
  509. }
  510. resolve(results);
  511. }
  512. return promise;
  513. }
  514. });
  515. });
  516. /**
  517. * A helper to create xhtml namespaced elements.
  518. */
  519. function createXHTMLElement(document, tagname) {
  520. return document.createElementNS("http://www.w3.org/1999/xhtml", tagname);
  521. }
  522. /**
  523. * A helper to go from a command context to a debugger panel.
  524. */
  525. function getPanel(context, id, options = {}) {
  526. if (!context) {
  527. return undefined;
  528. }
  529. let target = context.environment.target;
  530. if (options.ensureOpened) {
  531. return gDevTools.showToolbox(target, id).then(toolbox => {
  532. return toolbox.getPanel(id);
  533. });
  534. } else {
  535. let toolbox = gDevTools.getToolbox(target);
  536. if (toolbox) {
  537. return toolbox.getPanel(id);
  538. } else {
  539. return undefined;
  540. }
  541. }
  542. }
  543. /**
  544. * Converts a glob to a regular expression.
  545. */
  546. function globToRegExp(glob) {
  547. const reStr = glob
  548. // Escape existing regular expression syntax.
  549. .replace(/\\/g, "\\\\")
  550. .replace(/\//g, "\\/")
  551. .replace(/\^/g, "\\^")
  552. .replace(/\$/g, "\\$")
  553. .replace(/\+/g, "\\+")
  554. .replace(/\?/g, "\\?")
  555. .replace(/\./g, "\\.")
  556. .replace(/\(/g, "\\(")
  557. .replace(/\)/g, "\\)")
  558. .replace(/\=/g, "\\=")
  559. .replace(/\!/g, "\\!")
  560. .replace(/\|/g, "\\|")
  561. .replace(/\{/g, "\\{")
  562. .replace(/\}/g, "\\}")
  563. .replace(/\,/g, "\\,")
  564. .replace(/\[/g, "\\[")
  565. .replace(/\]/g, "\\]")
  566. .replace(/\-/g, "\\-")
  567. // Turn * into the match everything wildcard.
  568. .replace(/\*/g, ".*");
  569. return new RegExp("^" + reStr + "$");
  570. }