debugger.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  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 initPinBox() {
  36. document.querySelector(".pin-prompt form").addEventListener(
  37. "submit",
  38. function (event) {
  39. event.preventDefault();
  40. const pin = encodeURIComponent(this.pin.value);
  41. const encodedSecret = encodeURIComponent(SECRET);
  42. const btn = this.btn;
  43. btn.disabled = true;
  44. fetch(
  45. `${document.location.pathname}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
  46. )
  47. .then((res) => res.json())
  48. .then(({auth, exhausted}) => {
  49. if (auth) {
  50. EVALEX_TRUSTED = true;
  51. fadeOut(document.getElementsByClassName("pin-prompt")[0]);
  52. } else {
  53. alert(
  54. `Error: ${
  55. exhausted
  56. ? "too many attempts. Restart server to retry."
  57. : "incorrect pin"
  58. }`
  59. );
  60. }
  61. })
  62. .catch((err) => {
  63. alert("Error: Could not verify PIN. Network error?");
  64. console.error(err);
  65. })
  66. .finally(() => (btn.disabled = false));
  67. },
  68. false
  69. );
  70. }
  71. function promptForPin() {
  72. if (!EVALEX_TRUSTED) {
  73. const encodedSecret = encodeURIComponent(SECRET);
  74. fetch(
  75. `${document.location.pathname}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
  76. );
  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", "?__debugger__=yes&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. // Get input command.
  240. const cmd = command.value;
  241. // Setup GET request.
  242. const urlPath = "";
  243. const params = {
  244. __debugger__: "yes",
  245. cmd: cmd,
  246. frm: frameID,
  247. s: SECRET,
  248. };
  249. const paramString = Object.keys(params)
  250. .map((key) => {
  251. return "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
  252. })
  253. .join("");
  254. fetch(urlPath + "?" + paramString)
  255. .then((res) => {
  256. return res.text();
  257. })
  258. .then((data) => {
  259. const tmp = document.createElement("div");
  260. tmp.innerHTML = data;
  261. resolve(tmp);
  262. // Handle expandable span for long list outputs.
  263. // Example to test: list(range(13))
  264. let wrapperAdded = false;
  265. const wrapperSpan = document.createElement("span");
  266. const expansionButton = createExpansionButtonForConsole();
  267. tmp.querySelectorAll("span.extended").forEach((spanToWrap) => {
  268. const parentDiv = spanToWrap.parentNode;
  269. if (!wrapperAdded) {
  270. parentDiv.insertBefore(wrapperSpan, spanToWrap);
  271. wrapperAdded = true;
  272. }
  273. parentDiv.removeChild(spanToWrap);
  274. wrapperSpan.append(spanToWrap);
  275. spanToWrap.hidden = true;
  276. expansionButton.addEventListener("click", () => {
  277. spanToWrap.hidden = !spanToWrap.hidden;
  278. expansionButton.classList.toggle("open");
  279. return false;
  280. });
  281. });
  282. // Add expansion button at end of wrapper.
  283. if (wrapperAdded) {
  284. wrapperSpan.append(expansionButton);
  285. }
  286. })
  287. .catch((err) => {
  288. console.error(err);
  289. });
  290. return false;
  291. });
  292. }
  293. function fadeOut(element) {
  294. element.style.opacity = 1;
  295. (function fade() {
  296. element.style.opacity -= 0.1;
  297. if (element.style.opacity < 0) {
  298. element.style.display = "none";
  299. } else {
  300. requestAnimationFrame(fade);
  301. }
  302. })();
  303. }
  304. function fadeIn(element, display) {
  305. element.style.opacity = 0;
  306. element.style.display = display || "block";
  307. (function fade() {
  308. let val = parseFloat(element.style.opacity) + 0.1;
  309. if (val <= 1) {
  310. element.style.opacity = val;
  311. requestAnimationFrame(fade);
  312. }
  313. })();
  314. }
  315. function docReady(fn) {
  316. if (document.readyState === "complete" || document.readyState === "interactive") {
  317. setTimeout(fn, 1);
  318. } else {
  319. document.addEventListener("DOMContentLoaded", fn);
  320. }
  321. }