debugger.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. docReady(() => {
  2. if (!EVALEX_TRUSTED) {
  3. initPinBox();
  4. }
  5. // if we are in console mode, show the console.
  6. if (CONSOLE_MODE && EVALEX) {
  7. createInteractiveConsole();
  8. }
  9. const frames = document.querySelectorAll("div.traceback div.frame");
  10. if (EVALEX) {
  11. addConsoleIconToFrames(frames);
  12. }
  13. addEventListenersToElements(document.querySelectorAll("div.detail"), "click", () =>
  14. document.querySelector("div.traceback").scrollIntoView(false)
  15. );
  16. addToggleFrameTraceback(frames);
  17. addToggleTraceTypesOnClick(document.querySelectorAll("h2.traceback"));
  18. addInfoPrompt(document.querySelectorAll("span.nojavascript"));
  19. wrapPlainTraceback();
  20. });
  21. function addToggleFrameTraceback(frames) {
  22. frames.forEach((frame) => {
  23. frame.addEventListener("click", () => {
  24. frame.getElementsByTagName("pre")[0].parentElement.classList.toggle("expanded");
  25. });
  26. })
  27. }
  28. function wrapPlainTraceback() {
  29. const plainTraceback = document.querySelector("div.plain textarea");
  30. const wrapper = document.createElement("pre");
  31. const textNode = document.createTextNode(plainTraceback.textContent);
  32. wrapper.appendChild(textNode);
  33. plainTraceback.replaceWith(wrapper);
  34. }
  35. function makeDebugURL(args) {
  36. const params = new URLSearchParams(args)
  37. params.set("s", SECRET)
  38. return `?__debugger__=yes&${params}`
  39. }
  40. function initPinBox() {
  41. document.querySelector(".pin-prompt form").addEventListener(
  42. "submit",
  43. function (event) {
  44. event.preventDefault();
  45. const btn = this.btn;
  46. btn.disabled = true;
  47. fetch(
  48. makeDebugURL({cmd: "pinauth", pin: this.pin.value})
  49. )
  50. .then((res) => res.json())
  51. .then(({auth, exhausted}) => {
  52. if (auth) {
  53. EVALEX_TRUSTED = true;
  54. fadeOut(document.getElementsByClassName("pin-prompt")[0]);
  55. } else {
  56. alert(
  57. `Error: ${
  58. exhausted
  59. ? "too many attempts. Restart server to retry."
  60. : "incorrect pin"
  61. }`
  62. );
  63. }
  64. })
  65. .catch((err) => {
  66. alert("Error: Could not verify PIN. Network error?");
  67. console.error(err);
  68. })
  69. .finally(() => (btn.disabled = false));
  70. },
  71. false
  72. );
  73. }
  74. function promptForPin() {
  75. if (!EVALEX_TRUSTED) {
  76. fetch(makeDebugURL({cmd: "printpin"}));
  77. const pinPrompt = document.getElementsByClassName("pin-prompt")[0];
  78. fadeIn(pinPrompt);
  79. document.querySelector('.pin-prompt input[name="pin"]').focus();
  80. }
  81. }
  82. /**
  83. * Helper function for shell initialization
  84. */
  85. function openShell(consoleNode, target, frameID) {
  86. promptForPin();
  87. if (consoleNode) {
  88. slideToggle(consoleNode);
  89. return consoleNode;
  90. }
  91. let historyPos = 0;
  92. const history = [""];
  93. const consoleElement = createConsole();
  94. const output = createConsoleOutput();
  95. const form = createConsoleInputForm();
  96. const command = createConsoleInput();
  97. target.parentNode.appendChild(consoleElement);
  98. consoleElement.append(output);
  99. consoleElement.append(form);
  100. form.append(command);
  101. command.focus();
  102. slideToggle(consoleElement);
  103. form.addEventListener("submit", (e) => {
  104. handleConsoleSubmit(e, command, frameID).then((consoleOutput) => {
  105. output.append(consoleOutput);
  106. command.focus();
  107. consoleElement.scrollTo(0, consoleElement.scrollHeight);
  108. const old = history.pop();
  109. history.push(command.value);
  110. if (typeof old !== "undefined") {
  111. history.push(old);
  112. }
  113. historyPos = history.length - 1;
  114. command.value = "";
  115. });
  116. });
  117. command.addEventListener("keydown", (e) => {
  118. if (e.key === "l" && e.ctrlKey) {
  119. output.innerText = "--- screen cleared ---";
  120. } else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
  121. // Handle up arrow and down arrow.
  122. if (e.key === "ArrowUp" && historyPos > 0) {
  123. e.preventDefault();
  124. historyPos--;
  125. } else if (e.key === "ArrowDown" && historyPos < history.length - 1) {
  126. historyPos++;
  127. }
  128. command.value = history[historyPos];
  129. }
  130. return false;
  131. });
  132. return consoleElement;
  133. }
  134. function addEventListenersToElements(elements, event, listener) {
  135. elements.forEach((el) => el.addEventListener(event, listener));
  136. }
  137. /**
  138. * Add extra info
  139. */
  140. function addInfoPrompt(elements) {
  141. for (let i = 0; i < elements.length; i++) {
  142. elements[i].innerHTML =
  143. "<p>To switch between the interactive traceback and the plaintext " +
  144. 'one, you can click on the "Traceback" headline. From the text ' +
  145. "traceback you can also create a paste of it. " +
  146. (!EVALEX
  147. ? ""
  148. : "For code execution mouse-over the frame you want to debug and " +
  149. "click on the console icon on the right side." +
  150. "<p>You can execute arbitrary Python code in the stack frames and " +
  151. "there are some extra helpers available for introspection:" +
  152. "<ul><li><code>dump()</code> shows all variables in the frame" +
  153. "<li><code>dump(obj)</code> dumps all that's known about the object</ul>");
  154. elements[i].classList.remove("nojavascript");
  155. }
  156. }
  157. function addConsoleIconToFrames(frames) {
  158. for (let i = 0; i < frames.length; i++) {
  159. let consoleNode = null;
  160. const target = frames[i];
  161. const frameID = frames[i].id.substring(6);
  162. for (let j = 0; j < target.getElementsByTagName("pre").length; j++) {
  163. const img = createIconForConsole();
  164. img.addEventListener("click", (e) => {
  165. e.stopPropagation();
  166. consoleNode = openShell(consoleNode, target, frameID);
  167. return false;
  168. });
  169. target.getElementsByTagName("pre")[j].append(img);
  170. }
  171. }
  172. }
  173. function slideToggle(target) {
  174. target.classList.toggle("active");
  175. }
  176. /**
  177. * toggle traceback types on click.
  178. */
  179. function addToggleTraceTypesOnClick(elements) {
  180. for (let i = 0; i < elements.length; i++) {
  181. elements[i].addEventListener("click", () => {
  182. document.querySelector("div.traceback").classList.toggle("hidden");
  183. document.querySelector("div.plain").classList.toggle("hidden");
  184. });
  185. elements[i].style.cursor = "pointer";
  186. document.querySelector("div.plain").classList.toggle("hidden");
  187. }
  188. }
  189. function createConsole() {
  190. const consoleNode = document.createElement("pre");
  191. consoleNode.classList.add("console");
  192. consoleNode.classList.add("active");
  193. return consoleNode;
  194. }
  195. function createConsoleOutput() {
  196. const output = document.createElement("div");
  197. output.classList.add("output");
  198. output.innerHTML = "[console ready]";
  199. return output;
  200. }
  201. function createConsoleInputForm() {
  202. const form = document.createElement("form");
  203. form.innerHTML = "&gt;&gt;&gt; ";
  204. return form;
  205. }
  206. function createConsoleInput() {
  207. const command = document.createElement("input");
  208. command.type = "text";
  209. command.setAttribute("autocomplete", "off");
  210. command.setAttribute("spellcheck", false);
  211. command.setAttribute("autocapitalize", "off");
  212. command.setAttribute("autocorrect", "off");
  213. return command;
  214. }
  215. function createIconForConsole() {
  216. const img = document.createElement("img");
  217. img.setAttribute("src", makeDebugURL({cmd: "resource", f: "console.png"}));
  218. img.setAttribute("title", "Open an interactive python shell in this frame");
  219. return img;
  220. }
  221. function createExpansionButtonForConsole() {
  222. const expansionButton = document.createElement("a");
  223. expansionButton.setAttribute("href", "#");
  224. expansionButton.setAttribute("class", "toggle");
  225. expansionButton.innerHTML = "&nbsp;&nbsp;";
  226. return expansionButton;
  227. }
  228. function createInteractiveConsole() {
  229. const target = document.querySelector("div.console div.inner");
  230. while (target.firstChild) {
  231. target.removeChild(target.firstChild);
  232. }
  233. openShell(null, target, 0);
  234. }
  235. function handleConsoleSubmit(e, command, frameID) {
  236. // Prevent page from refreshing.
  237. e.preventDefault();
  238. return new Promise((resolve) => {
  239. fetch(makeDebugURL({cmd: command.value, frm: frameID}))
  240. .then((res) => {
  241. return res.text();
  242. })
  243. .then((data) => {
  244. const tmp = document.createElement("div");
  245. tmp.innerHTML = data;
  246. resolve(tmp);
  247. // Handle expandable span for long list outputs.
  248. // Example to test: list(range(13))
  249. let wrapperAdded = false;
  250. const wrapperSpan = document.createElement("span");
  251. const expansionButton = createExpansionButtonForConsole();
  252. tmp.querySelectorAll("span.extended").forEach((spanToWrap) => {
  253. const parentDiv = spanToWrap.parentNode;
  254. if (!wrapperAdded) {
  255. parentDiv.insertBefore(wrapperSpan, spanToWrap);
  256. wrapperAdded = true;
  257. }
  258. parentDiv.removeChild(spanToWrap);
  259. wrapperSpan.append(spanToWrap);
  260. spanToWrap.hidden = true;
  261. expansionButton.addEventListener("click", (event) => {
  262. event.preventDefault();
  263. spanToWrap.hidden = !spanToWrap.hidden;
  264. expansionButton.classList.toggle("open");
  265. return false;
  266. });
  267. });
  268. // Add expansion button at end of wrapper.
  269. if (wrapperAdded) {
  270. wrapperSpan.append(expansionButton);
  271. }
  272. })
  273. .catch((err) => {
  274. console.error(err);
  275. });
  276. return false;
  277. });
  278. }
  279. function fadeOut(element) {
  280. element.style.opacity = 1;
  281. (function fade() {
  282. element.style.opacity -= 0.1;
  283. if (element.style.opacity < 0) {
  284. element.style.display = "none";
  285. } else {
  286. requestAnimationFrame(fade);
  287. }
  288. })();
  289. }
  290. function fadeIn(element, display) {
  291. element.style.opacity = 0;
  292. element.style.display = display || "block";
  293. (function fade() {
  294. let val = parseFloat(element.style.opacity) + 0.1;
  295. if (val <= 1) {
  296. element.style.opacity = val;
  297. requestAnimationFrame(fade);
  298. }
  299. })();
  300. }
  301. function docReady(fn) {
  302. if (document.readyState === "complete" || document.readyState === "interactive") {
  303. setTimeout(fn, 1);
  304. } else {
  305. document.addEventListener("DOMContentLoaded", fn);
  306. }
  307. }