/** ########################################################################### * Frontend Queries * ##########################################################################*/ const getPauseId = window.getPauseId = () => { const state = app.store.getState(); const pauseId = state?.pause?.id; if (!pauseId) { throw new Error(`Pause required (but not found) for snippet`); } return pauseId; }; const getSessionId = window.getSessionId = () => { const state = app.store.getState(); const sessionId = state?.app?.sessionId; if (!sessionId) { throw new Error(`sessionId required (but not found) for snippet`); } return sessionId; }; // (() => { // const state = app.store.getState(); // const sessionId = state?.app?.sessionId; // if (!sessionId) { // throw new Error(`sessionId required (but not found) for snippet`); // } // return sessionId; // })() const getAllFramesForPause = window.getAllFramesForPause = async (pauseId) => { return await app.client.Pause.getAllFrames( {}, window.getSessionId(), pauseId || window.getPauseId() ); }; /** ########################################################################### * point stuff * ##########################################################################*/ const InvalidCheckpointId = 0; const FirstCheckpointId = 1; /** * Copied from backend/src/shared/point.ts */ function pointToBigInt(point) { let rv = BigInt(0); let shift = 0; if (point.position) { addValue(point.position.offset || 0, 32); switch (point.position.kind) { case "EnterFrame": addValue(0, 3); break; case "OnStep": addValue(1, 3); break; // NOTE: In the past, "2" here indicated an "OnThrow" step type. case "OnPop": addValue(3, 3); break; case "OnUnwind": addValue(4, 3); break; default: throw new Error("UnexpectedPointPositionKind " + point.position.kind); } // Deeper frames predate shallower frames with the same progress counter. console.assert( point.position.frameIndex !== undefined, "Point should have a frameIndex", { point, } ); addValue((1 << 24) - 1 - point.position.frameIndex, 24); // Points with positions are later than points with no position. addValue(1, 1); } else { addValue(point.bookmark || 0, 32); addValue(0, 3 + 24 + 1); } addValue(point.progress, 48); // Subtract here so that the first point in the recording is 0 as reflected // in the protocol definition. addValue(point.checkpoint - FirstCheckpointId, 32); return rv; function addValue(v, nbits) { rv |= BigInt(v) << BigInt(shift); shift += nbits; } } function BigIntToPoint(n) { const offset = readValue(32); const kindValue = readValue(3); const indexValue = readValue(24); const hasPosition = readValue(1); const progress = readValue(48); const checkpoint = readValue(32) + FirstCheckpointId; if (!hasPosition) { if (offset) { return { checkpoint, progress, bookmark: offset }; } return { checkpoint, progress }; } let kind; switch (kindValue) { case 0: kind = "EnterFrame"; break; case 1: kind = "OnStep"; break; case 2: ThrowError("UnexpectedOnThrowPoint", { point: n.toString() + "n" }); break; case 3: kind = "OnPop"; break; case 4: kind = "OnUnwind"; break; } const frameIndex = (1 << 24) - 1 - indexValue; return { checkpoint, progress, position: { kind, offset, frameIndex }, }; function readValue(nbits) { const mask = (BigInt(1) << BigInt(nbits)) - BigInt(1); const rv = Number(n & mask); n = n >> BigInt(nbits); return rv; } } /** ########################################################################### * Code serialization utilities * ##########################################################################*/ function serializeFunctionCall(f) { var code = `(eval(eval(${JSON.stringify(f.toString())})))`; code = `(${code})()`; return JSON.stringify(`dev:${code}`); } function testRunSerializedExpressionLocal(expression) { // NOTE: Extra parentheses are added in frontend sometimes expression = `(${expression})`; var cmd = expression; if (cmd.startsWith('(')) { // strip "()" cmd = cmd.substring(1, expression.length - 1); } // parse JSON (used for serialization) cmd = JSON.parse(cmd); // strip "dev:" and run cmd = `(${cmd.substring(4)})`; eval(cmd); } /** ########################################################################### * {@link chromiumEval} executes arbitrary code inside `chromium` * ##########################################################################*/ window.chromiumEval = async (expression) => { if (expression instanceof Function) { // serialize function expression = serializeFunctionCall(expression); } const x = await app.client.Pause.evaluateInGlobal( { expression, pure: false, }, getSessionId(), getPauseId() ); try { } catch (err) { console.error(`unable to parse returned value:`, x, '\n\n'); throw err; } const { result: { data, returned: { value } = {}, exception: { value: errValue } = {} } } = x; if (errValue) { throw new Error(errValue); } return value; }; /** ########################################################################### * util * ##########################################################################*/ window.flushCommandErrors = async () => { let err // NOTE: can cause infinite loop if `chromiumEval` itself induces errors while ((err = await chromiumEval(() => DevOnly.popCommandError()))) { console.error(err); } }; /** ########################################################################### * DOM protocol queries * ##########################################################################*/ async function getAllBoundingClientRects() { try { const { elements } = await app.client.DOM.getAllBoundingClientRects( {}, getSessionId(), getPauseId() ); return elements; } finally { await flushCommandErrors(); } }; async function getBoxModel(node) { try { const result = await app.client.DOM.getBoxModel( { node }, getSessionId(), getPauseId() ); return result; } finally { await flushCommandErrors(); } } async function DOM_getDocument() { try { const result = await app.client.DOM.getDocument( {}, getSessionId(), getPauseId() ); return result; } finally { await flushCommandErrors(); } } /** ########################################################################### * High-level tools. * ##########################################################################*/ let lastCreatedPause; function getCreatedPause() { return lastCreatedPause; } async function pauseAt(pointStruct) { const point = pointToBigInt(pointStruct).toString(); lastCreatedPause = await app.client.Session.createPause({ point }, getSessionId()); console.log(`Paused at ${lastCreatedPause?.pauseId}:`, lastCreatedPause); } async function getAllFrames() { const pause = getCreatedPause(); if (!pause?.pauseId) { throw new Error(`Not paused at a good point.`); } if (pause?.pauseId) { const res = await getAllFramesForPause(pause.pauseId); console.log(`getAllFrames:`, res); } } async function getTopFrame() { const { data: { frames } } = await app.client.Pause.getAllFrames({}, sessionId, getPauseId()); const topFrame = frames[0]; return topFrame; } function getSelectedLocation() { const loc = app.store.getState().sources?.selectedLocation; if (!loc) { throw new Error(`No source selected`); } return loc; } function getSelectedSourceId() { return getSelectedLocation().sourceId; } async function selectLocation(sourceId, loc = undefined) { return app.actions.selectLocation(loc, { sourceId: sourceId + "" }); } function getSourceText(line) { return document.querySelector(`[data-test-id="SourceLine-${line}"] [data-test-formatted-source="true"]`).textContent; } // getSourceText(24713); /** ########################################################################### * DOM Manipulation * ##########################################################################*/ function getElTestString(el, name) { return el.getAttribute(`${name}`); } function getElTestNumber(el, name) { return parseInt(el.getAttribute(`${name}`)); } function isElVisible(e) { return !!( e.offsetWidth || e.offsetHeight || e.getClientRects().length ); } function getVisibleLineEls() { const lineEls = Array.from(document.querySelectorAll("[data-test-line-number]")).filter(isElVisible); const lineNums = lineEls.map(el => getElTestNumber(el, "data-test-line-number")); return { lineEls, lineNums }; } /** * @example getSourceLineChildElements(24713).columnEls[1] */ function getSourceLineChildElements(line) { const lineEl = document.querySelector(`[data-test-line-number="${line}"`); if (!lineEl) { return null; } const columnEls = Array.from(lineEl.querySelectorAll(`[data-column-index]`)) if (!columnEls.length) { return null; } const columnIndexes = columnEls.map(el => getElTestNumber(el, "data-column-index")); return { columnEls, columnIndexes } } function insertIntoString(str, idx, toInsert) { return str.slice(0, idx) + toInsert + str.slice(idx); } function reset() { removeCustomEls(); } function removeCustomEls() { const customEls = Array.from(document.querySelectorAll("[data-custom]")); for (const el of customEls) { el.remove(); } } async function insertSourceBreakpoints() { removeCustomEls(); const loc = app.store.getState().sources?.selectedLocation; if (!loc) { throw new Error(`insertSourceBreakpoints requires selected location`); } const { lineLocations } = await app.client.Debugger.getPossibleBreakpoints( { sourceId: loc.sourceId }, sessionId ); // const sourceIdNum = loc.sourceId.match(/\d+/)[0]; // const sources = app.store.getState().sources.sourceDetails.entities[sourceIdNum]; // console.log(sources); // const lineMin = nLineFrom; // const lineMax = lineMin + nLineDelta; // const locs = lineLocations.filter(l => l.line >= lineMin && l.line <= lineMax); const breakpointLocsByLine = Object.fromEntries(lineLocations.map(loc => [loc.line, loc])); // const breakpointLocations = locs.flatMap(l => { // return l.columns?.map(c => `${l.line}:${c}`) || ""; // }); const { lineEls, lineNums } = getVisibleLineEls(); for (let j = 0; j < lineEls.length; ++j) { const line = lineNums[j]; const breaks = breakpointLocsByLine[line]; if (breaks) { // Modify line const sourceEls = getSourceLineChildElements(line); if (!sourceEls) { continue; } // if (line === 24713) // debugger; const { columnEls, columnIndexes } = sourceEls; for (let col of breaks.columns) { // Iterate column back to front, so modification does not mess with follow-up indexes. const iCol = columnIndexes.findLastIndex((idx, i) => idx < col && (i === columnIndexes.length || columnIndexes[i+1] >= col)); const targetEl = columnEls[iCol]; const iOffset = col - columnIndexes[iCol]; if (!targetEl) { debugger; continue; } targetEl.innerHTML = insertIntoString(targetEl.innerHTML, iOffset, `<span data-custom="1" style="color: red">|</span>`); } } } // locs.forEach(l => { // const source = getSourceText(l.line); // const highlightCss = "color: red"; // const clearCss = "color: default"; // let lastIdx = 0; // const sourceParts = l.columns.map((col, i) => { // return source.slice(lastIdx, lastIdx = l.columns[i]); // }); // sourceParts.push(source.slice(lastIdx)); // const format = sourceParts.join("<BREAK>"); // console.log(format); // }); // console.log(...printArgs); // return { // breakpointLocations // }; } async function getHitCounts() { const loc = getSelectedLocation(); console.log("loc", loc); const minCol = 0; const maxCol = 100; const { lineLocations } = await app.client.Debugger.getHitCounts( { sourceId: loc.sourceId, locations: [{ line: loc.line, columns: range(minCol, maxCol) }] }, sessionId ); return lineLocations; } async function getCorrespondingSources() { function getCorrespondingSources(name) { const sources = Object.values(app.store.getState().sources.sourceDetails.entities); // TODO: Also look up source-mapped sources? return sources.filter(s => s.url?.endsWith(name)); } console.log(getCorrespondingSources("_app-96d565375e549a2c.js")); } /** ########################################################################### * some things we want to play around with * ##########################################################################*/ async function main() { // const pointStruct = JSON.parse("{\"checkpoint\":12,\"progress\":35935,\"position\":{\"kind\":\"OnPop\",\"offset\":0,\"frameIndex\":4,\"functionId\":\"28:1758\"}}"); // RUN-1576 // http://admin/crash/controller/dff404d0-e114-4622-8daa-1c3340ba7833 // const pointStruct = { // "checkpoint": 86, // "progress": 44196840, // "bookmark": 2149 // }; // await pauseAt(pointStruct); // await getAllFrames(); console.log(await getSelectedSourceId()); // await selectLocation(sourceId); insertSourceBreakpoints(); // copy(BigIntToPoint(27584077111921759048236340451739749n)); } main(); // const initTimer = setInterval(() => { // console.log("initTimer checking..."); // if (window.app) { // clearInterval(initTimer); // main(); // } // }, 100);