diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 04ea8a3055c43d..32d187e66e5f1b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -216,6 +216,7 @@ steps: depends_on: - "build-chromium-linux-x86_64" build: + branch: dominik/tt-1261-recorder-crashes-from-save-examplests-are-not-uploaded env: OS: "linux" ARCH: "x86_64" @@ -231,6 +232,7 @@ steps: depends_on: - "build-chromium-mac-arm64" build: + branch: dominik/tt-1261-recorder-crashes-from-save-examplests-are-not-uploaded env: OS: "macos" ARCH: "arm64" diff --git a/DEPS b/DEPS index 906944d6b3b8ee..09822737d4237b 100644 --- a/DEPS +++ b/DEPS @@ -311,7 +311,7 @@ vars = { # Three lines of non-changing comments so that # the commit queue can handle CLs rolling V8 # and whatever else without interference from each other. - 'v8_revision': '7e105ba00a264f31ed04d5bada4d3aa63be1e0f0', + 'v8_revision': 'ea1ffeb18c92e9a1c7f3fda7307bc7c9825d1fb5', # Three lines of non-changing comments so that # the commit queue can handle CLs rolling swarming_client # and whatever else without interference from each other. diff --git a/REPLAY_BACKEND_REV b/REPLAY_BACKEND_REV index 4b41b48b53e30e..3ee60a81afb1ea 100644 --- a/REPLAY_BACKEND_REV +++ b/REPLAY_BACKEND_REV @@ -1 +1 @@ -9e69b244fd0973ea3f8fb46fc9eb2ce39fa615a8 +63be1e1d8a17bdfe09f570f2eed77136ec29ae81 diff --git a/base/logging.cc b/base/logging.cc index dfa8c2bc1eae7d..2c948edf22a59d 100644 --- a/base/logging.cc +++ b/base/logging.cc @@ -127,6 +127,8 @@ typedef FILE* FileHandle; #include #endif +static void* REPLAY_NO_HANDLE = reinterpret_cast(1); + static void* LookupRecordReplaySymbol(const char* name) { #if !BUILDFLAG(IS_WIN) void* fnptr = dlsym(RTLD_DEFAULT, name); @@ -134,22 +136,37 @@ static void* LookupRecordReplaySymbol(const char* name) { HMODULE module = GetModuleHandleA("windows-recordreplay.dll"); void* fnptr = module ? (void*)GetProcAddress(module, name) : nullptr; #endif - return fnptr ? fnptr : reinterpret_cast(1); + return fnptr ? fnptr : REPLAY_NO_HANDLE; } -static void RecordReplayPrint(const char* aFormat, ...) { - static void* fnptr; - if (!fnptr) { - fnptr = LookupRecordReplaySymbol("RecordReplayPrint"); +static void* g_record_replay_print_ptr; +static bool HasReplayModule() { + if (!g_record_replay_print_ptr) { + g_record_replay_print_ptr = LookupRecordReplaySymbol("RecordReplayPrint"); } - if (fnptr != reinterpret_cast(1)) { + return g_record_replay_print_ptr != REPLAY_NO_HANDLE; +} + +static void RecordReplayPrint(const char* aFormat, ...) { + if (HasReplayModule()) { va_list ap; va_start(ap, aFormat); - reinterpret_cast(fnptr)(aFormat, ap); + reinterpret_cast(g_record_replay_print_ptr)(aFormat, ap); va_end(ap); } } +static bool RecordReplayIsReplaying() { + static void* fnptr; + if (!fnptr) { + fnptr = LookupRecordReplaySymbol("RecordReplayIsReplaying"); + } + if (fnptr != REPLAY_NO_HANDLE) { + return reinterpret_cast(fnptr)(); + } + return false; +} + namespace logging { namespace { @@ -743,7 +760,10 @@ LogMessage::~LogMessage() { size_t stack_start = stream_.str().length(); #if !defined(OFFICIAL_BUILD) && !BUILDFLAG(IS_NACL) && !defined(__UCLIBC__) && \ !BUILDFLAG(IS_AIX) - if (severity_ == LOGGING_FATAL && !base::debug::BeingDebugged()) { + + // Prevent calling |BeingDebugged| since it tries to create a debugger process. + // That attempt will cause a crash when replaying when events are disallowed. + if (severity_ == LOGGING_FATAL && !RecordReplayIsReplaying() && !base::debug::BeingDebugged()) { // Include a stack trace on a fatal, unless a debugger is attached. base::debug::StackTrace stack_trace; stream_ << std::endl; // Newline to separate from log message. @@ -766,8 +786,15 @@ LogMessage::~LogMessage() { #endif stream_ << std::endl; std::string str_newline(stream_.str()); - - RecordReplayPrint("LogMessage %s", str_newline.c_str()); + + if (severity_ == LOGGING_FATAL) { + // TODO: We actually want to crash with the right reason, but we don't have + // access to V8RecordReplayCrash. + // RecordReplayCrash("FATAL: %s", str_newline.c_str()); + RecordReplayPrint("FATAL: %s", str_newline.c_str()); + } else { + RecordReplayPrint("LogMessage [%d/%d] %s", severity_, LOGGING_NUM_SEVERITIES-1, str_newline.c_str()); + } TRACE_LOG_MESSAGE( file_, base::StringPiece(str_newline).substr(message_start_), line_); @@ -981,7 +1008,7 @@ LogMessage::~LogMessage() { // information, and displaying message boxes when the application is // hosed can cause additional problems. #ifndef NDEBUG - if (!base::debug::BeingDebugged()) { + if (!RecordReplayIsReplaying() && !base::debug::BeingDebugged()) { // Displaying a dialog is unnecessary when debugging and can complicate // debugging. DisplayDebugMessageInDialog(stream_.str()); diff --git a/build.js b/build.js index 6b0e86b887d24b..708ccaa0b2c7e9 100644 --- a/build.js +++ b/build.js @@ -167,11 +167,12 @@ function spawnChecked(cmd, args, options) { const rv = spawnSync(cmd, args, options); - if (rv.status != 0 || rv.error) { + if (rv.status || rv.error || rv.signal) { + const errMsg = ` Spawned process "${prettyCmd}" failed: status=${rv.status || ""}, signal=${rv.signal || ""}, err=${rv.error || ""}`; console.error( - `Process failed: err=${rv.error || ""}, signal=${rv.signal || ""}` + errMsg ); - throw new Error(`Spawned process failed with exit code ${rv.status}`); + throw new Error(errMsg); } return rv; diff --git a/replay-assets/replay_command_handlers.js b/replay-assets/replay_command_handlers.js index 4c0ec646cba408..74c3999f38f5d7 100644 --- a/replay-assets/replay_command_handlers.js +++ b/replay-assets/replay_command_handlers.js @@ -14,9 +14,6 @@ const { hasDiverged, setCDPMessageCallback, sendCDPMessage: sendCDPMessageRaw, - setCommandCallback, - setClearPauseDataCallback, - addNewScriptHandler, getCurrentError, layoutDom, @@ -53,7 +50,7 @@ const { // utils.js /////////////////////////////////////////////////////////////////////////////// -// Some of these are duplicated in gSourceMapScript, so watch out when making +// Some of these are duplicated in replay_sourcemap_handler, so watch out when making // modifications to update both versions... function isFunction(val) { @@ -144,13 +141,11 @@ function isValidBaseURL(url) { } /////////////////////////////////////////////////////////////////////////////// -// message.js +// CDP Handlers. /////////////////////////////////////////////////////////////////////////////// -function initMessages() { +function initCdp() { setCDPMessageCallback(messageCallback); - setCommandCallback(commandCallback); - setClearPauseDataCallback(clearPauseDataCallback); } let gNextMessageId = 1; @@ -232,7 +227,7 @@ function sendCDPMessage(method, params) { const sendMessage = sendCDPMessage; -function addEventListener(method, callback) { +function addCDPEventListener(method, callback) { gEventListeners.set(method, callback); } @@ -323,19 +318,23 @@ function executeCommand(method, params) { VerboseCommands && log(`[Command ${method}] Handling command, params=${JSON_stringify(params)}...`); const result = CommandCallbacks[method](params); VerboseCommands && log(`[Command ${method}] Handled command, result=${JSON_stringify(result)}`); + if (!result) { + // NOTE: CommandCallback expects a result. + throw new Error(`[Command ${method}] Did not return a result.`); + } return result; } -function commandCallback(method, params) { - if (!CommandCallbacks[method]) { - log(`[RuntimeError][Command ${method}] Missing command callback: ${method}`); +function commandCallback(commandName, params) { + if (!CommandCallbacks[commandName]) { + log(`[RuntimeError][Command ${commandName}] Missing command callback: ${commandName}`); return {}; } try { - return executeCommand(method, params); + return executeCommand(commandName, params); } catch (e) { - log(`[RuntimeError][Command ${method}]${getAliveLabel()} ${e?.stack || e}`); + log(`[RuntimeError][Command ${commandName}]${getAliveLabel()} ${e?.stack || e}`); // Pass the error up to V8; it can (for now) decide how to handle itself, whether // it should crash or not, etc. Eventually, the caller of the command should make // that decision. @@ -426,21 +425,6 @@ function Target_getCurrentMessageContents() { }; } -addNewScriptHandler((scriptId, sourceURL, relativeSourceMapURL) => { - if (!relativeSourceMapURL) - return; - - const urls = getSourceMapURLs(sourceURL, relativeSourceMapURL); - if (!urls) - return; - - const { sourceMapURL, sourceMapBaseURL } = urls; - gSourceMapData.set(scriptId, { - url: sourceMapURL, - baseUrl: sourceMapBaseURL - }); -}, /* disallowEvents */ true); - function Target_getSourceMapURL({ sourceId }) { return gSourceMapData.get(sourceId) || {}; } @@ -3317,28 +3301,52 @@ function wrapReplayApiFunction(fn) { }; } +/** ########################################################################### + * ReplayJs: Internal event handling. + * ##########################################################################*/ + +function handleNewScript(scriptId, sourceURL, relativeSourceMapURL) { + if (!relativeSourceMapURL) + return; + + const urls = getSourceMapURLs(sourceURL, relativeSourceMapURL); + if (!urls) + return; + + const { sourceMapURL, sourceMapBaseURL } = urls; + gSourceMapData.set(scriptId, { + url: sourceMapURL, + baseUrl: sourceMapBaseURL + }); +} + +function initializeReplayJsEvents(ReplayJsEventEmitter) { + ReplayJsEventEmitter.on("command", commandCallback); + ReplayJsEventEmitter.on("clearPauseDataCallback", clearPauseDataCallback); + ReplayJsEventEmitter.on("newScriptEventsDisallowed", handleNewScript); +} /////////////////////////////////////////////////////////////////////////////// // main.js /////////////////////////////////////////////////////////////////////////////// patchReplayApi(); -initMessages(); -addEventListener("Runtime.consoleAPICalled", onConsoleAPICall); -addEventListener("Runtime.executionContextCreated", ({ context }) => { +initCdp(); +addCDPEventListener("Runtime.consoleAPICalled", onConsoleAPICall); +addCDPEventListener("Runtime.executionContextCreated", ({ context }) => { gExecutionContexts.set(context.id, context); for (const callback of gContextChangeCallbacks) { callback(context, "add"); } }); -addEventListener("Runtime.executionContextDestroyed", ({ executionContextId }) => { +addCDPEventListener("Runtime.executionContextDestroyed", ({ executionContextId }) => { const context = gExecutionContexts.get(executionContextId); for (const callback of gContextChangeCallbacks) { callback(context, "remove"); } gExecutionContexts.delete(executionContextId); }); -addEventListener("Runtime.executionContextsCleared", () => { +addCDPEventListener("Runtime.executionContextsCleared", () => { for (const context of gExecutionContexts.values()) { for (const callback of gContextChangeCallbacks) { callback(context, "remove"); @@ -3348,4 +3356,6 @@ addEventListener("Runtime.executionContextsCleared", () => { }); sendCDPMessage("Runtime.enable"); -})(); +// Script return value +return initializeReplayJsEvents; +})() diff --git a/replay-assets/replay_init.js b/replay-assets/replay_init.js new file mode 100644 index 00000000000000..5da7b06d6ecb21 --- /dev/null +++ b/replay-assets/replay_init.js @@ -0,0 +1,37 @@ +(() => { +/** ########################################################################### + * ReplayJs: Internal event handling. + * ##########################################################################*/ + +const ReplayJsEventEmitterPrototype = { + on(event, cb) { + this._callbacks[event] ||= []; + this._callbacks[event].push(cb); + }, + + emit(event, ...args) { + let cbs = this._callbacks[event]; + if (cbs) { + cbs.forEach((cb) => cb(...args)); + } + }, + + emitWithResult(event, ...args) { + let cbs = this._callbacks[event]; + if (!cbs?.length) { + // If the caller expects a return value, there must be at least one callback registered. + throw new Error(`ReplayJsEvent_emitWithResult_failed_unknown_event: ${event}`); + } + const rv = cbs[0](...args); + return rv; + }, +}; + +function initializeReplayJsEvents(ReplayJsEventEmitter) { + // Set up the event emitter. + Object.assign(ReplayJsEventEmitter, ReplayJsEventEmitterPrototype); + ReplayJsEventEmitter._callbacks = {}; +} + +return initializeReplayJsEvents; +})(); diff --git a/replay-assets/replay_sourcemap_handler.js b/replay-assets/replay_sourcemap_handler.js new file mode 100644 index 00000000000000..ca23cb7257d6d4 --- /dev/null +++ b/replay-assets/replay_sourcemap_handler.js @@ -0,0 +1,273 @@ +// replay_sourcemap_handler.js +(() => { + +// Avoid monkey patching. +const fetch = window.fetch; +const DateNow = Date.now; + +const { + log, + warning, + getRecordingId, + sha256DigestHex, + writeToRecordingDirectory, + addRecordingEvent, + getScriptSource, + recordingDirectoryFileExists, + readFromRecordingDirectory, + getRecordingFilePath, + RECORD_REPLAY_DISABLE_SOURCEMAP_CACHE, +} = __RECORD_REPLAY_ARGUMENTS__; + +const cache = {}; + +// Provide a cache for urls, salted with the supplied hash. Practically, this +// means if the script content changes at the url, we will re-download the resource. +async function getCachedResource(url, hash) { + const key = `${url}:${hash}`; + if (cache[key] && !RECORD_REPLAY_DISABLE_SOURCEMAP_CACHE) { + return cache[key]; + } + + log(`fetching sourcemap resource ${key}`); + + const res = await fetchText(url); + cache[key] = res; + return res; +} + +async function handleNewScript(scriptId, sourceURL, relativeSourceMapURL) { + try { + if (!relativeSourceMapURL || relativeSourceMapURL.startsWith("data:")) + return; + + const recordingId = getRecordingId(); + if (!recordingId) { + // The recording has been invalidated. + return; + } + + const urls = getSourceMapURLs(sourceURL, relativeSourceMapURL); + if (!urls) return; + + const scriptSource = getScriptSource(scriptId); + const scriptHash = sha256DigestHex(scriptSource); + + const { sourceMapURL, sourceMapBaseURL } = urls; + + let sourceMap; + try { + sourceMap = await getCachedResource(sourceMapURL, scriptHash); + } catch (err) { + log( + `[RuntimeError] Failed to read sourcemap ${sourceMapURL}: ${err.message}` + ); + } + if (!sourceMap) { + return; + } + + const id = scriptHash; + const name = `sourcemap-${id}.map`; + const lookupName = `sourcemap-${id}.lookup`; + + let sources; + if ( + recordingDirectoryFileExists(name) && + recordingDirectoryFileExists(lookupName) + ) { + try { + sources = JSON.parse(readFromRecordingDirectory(lookupName)); + } catch (err) { + log( + `[RuntimeError][sourcemaps] Failed to load sourcemaps from file: ${lookupName} - ${err.message}` + ); + } + } + + if (!sources) { + writeToRecordingDirectory(name, sourceMap); + + sources = collectUnresolvedSourceMapResources( + sourceMap, + sourceMapURL, + sourceURL + ); + writeToRecordingDirectory(lookupName, JSON.stringify(sources)); + } + + addRecordingEvent( + JSON.stringify({ + kind: "sourcemapAdded", + path: getRecordingFilePath(name), + recordingId, + id, + url: sourceMapURL, + baseURL: sourceMapBaseURL, + targetContentHash: `sha256:${scriptHash}`, + targetURLHash: sourceURL ? makeAPIHash(sourceURL) : undefined, + targetMapURLHash: makeAPIHash(sourceMapURL), + timestamp: DateNow(), + }) + ); + + for (const { offset, url } of sources) { + let sourceContent; + try { + sourceContent = await getCachedResource(url, scriptHash); + } catch (err) { + log( + `[RuntimeError][sourcemaps] Failed to read original source ${url}: ${err.message}` + ); + continue; + } + const hash = sha256DigestHex(sourceContent); + const name = `source-${hash}`; + + if (!recordingDirectoryFileExists(name)) { + writeToRecordingDirectory(name, sourceContent); + } + addRecordingEvent( + JSON.stringify({ + kind: "originalSourceAdded", + path: getRecordingFilePath(name), + recordingId, + parentId: id, + parentOffset: offset, + timestamp: DateNow(), + }) + ); + } + } catch (err) { + warning(`[RuntimeError][sourcemaps] Exception - ${err?.stack || err}`); + } +} + +function initializeEvents(ReplayJsEventEmitter) { + ReplayJsEventEmitter.on("newScript", handleNewScript); +} + +async function fetchText(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `Fetching ${url} failed with status code ${response.status} (${response.statusText})` + ); + } + return await response.text(); +} + +function makeAPIHash(content) { + assert(typeof content === "string"); + const digestHex = sha256DigestHex(content); + return "sha256:" + digestHex; +} + +function collectUnresolvedSourceMapResources(mapText, mapURL) { + let obj; + let sourceOffset = 0; + + function logError(msg) { + log(`[RuntimeError][sourcemaps] ${msg} (${mapURL}:${sourceOffset})`); + } + + try { + obj = JSON.parse(mapText); + if (typeof obj !== "object" || !obj) { + return []; + } + } catch (err) { + logError( + `Exception parsing sourcemap JSON (${mapURL}): ${err?.message || err}` + ); + return []; + } + + const unresolvedSources = []; + if (obj.version !== 3) { + logError("Invalid sourcemap version: " + obj.version); + return []; + } + + if (obj.sources != null) { + const { sourceRoot, sources, sourcesContent } = obj; + + if (Array.isArray(sources)) { + for (let i = 0; i < sources.length; i++) { + const offset = sourceOffset++; + + if ( + !Array.isArray(sourcesContent) || + typeof sourcesContent[i] !== "string" + ) { + let url = sources[i]; + if (typeof sourceRoot === "string" && sourceRoot) { + url = sourceRoot.replace(/\/?/, "/") + url; + } + let sourceURL; + try { + sourceURL = new URL(url, mapURL).toString(); + } catch { + logError("Unable to compute original source URL: " + url); + continue; + } + + unresolvedSources.push({ + offset, + url: sourceURL, + }); + } + } + } else { + logError("Invalid sourcemap sources list"); + } + } + + return unresolvedSources; +} + +function assert(v, msg = "") { + if (!v) { + const m = `Assertion failed when handling command (${msg})`; + log(`[RuntimeError] ${m} - ${Error().stack}`); + throw new Error(m); + } +} + +function getSourceMapURLs(sourceURL, relativeSourceMapURL) { + let sourceBaseURL; + if (typeof sourceURL === "string" && isValidBaseURL(sourceURL)) { + sourceBaseURL = sourceURL; + } else if (window?.location?.href && isValidBaseURL(window?.location?.href)) { + sourceBaseURL = window.location.href; + } + + let sourceMapURL; + try { + sourceMapURL = new URL(relativeSourceMapURL, sourceBaseURL).toString(); + } catch (err) { + log("Failed to process sourcemap url: " + err.message); + return null; + } + + // If the map was a data: URL or something along those lines, we want + // to resolve paths in the map relative to the overall base. + const sourceMapBaseURL = isValidBaseURL(sourceMapURL) + ? sourceMapURL + : sourceBaseURL; + + return { sourceMapURL, sourceMapBaseURL }; +} + +function isValidBaseURL(url) { + try { + new URL("", url); + return true; + } catch { + return false; + } +} + +// Script return value +return initializeEvents; +})(); diff --git a/replay_build_scripts/lint.mjs b/replay_build_scripts/lint.mjs index 95afb90db868a4..936ee787f3547c 100644 --- a/replay_build_scripts/lint.mjs +++ b/replay_build_scripts/lint.mjs @@ -24,7 +24,7 @@ function findMatches(text /*: string*/, regex /*: RegExp*/) { function extractNamedScriptBlock( text /*: string*/, start /*: number*/, - end /*: number*/, + end /*: number*/ ) { const lines = text.split("\n"); const name = lines[start].split(" ")[2]; @@ -66,7 +66,10 @@ function calculateStatsPerFile(messages) { return stat; } -async function lintScript(fpath, { name, text } /*: { name: string, text: string }*/) { +async function lintScript( + fpath, + { name, text } /*: { name: string, text: string }*/ +) { const messages = linter.verify(text, { parserOptions: { ecmaVersion: 2023, @@ -135,22 +138,35 @@ async function lintScript(fpath, { name, text } /*: { name: string, text: string return { errorCount, fatalErrorCount, warningCount }; } +const AssetFiles = [ + "replay_init.js", + "replay_command_handlers.js", + "replay_sourcemap_handler.js", +]; + +let totalErrorCount = 0; +let totalWarningCount = 0; + async function main() { await lintFile( path.join( ROOT_DIR, - "third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc", + "third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc" ), /R""""\(/g, /\)""""/g ); - await lintFile( - path.join( - ROOT_DIR, - "replay-assets/replay_command_handlers.js", - ) - ); + for (const jsFile of AssetFiles) { + await lintFile(path.join(ROOT_DIR, "replay-assets/" + jsFile)); + } + + const bad = !!totalErrorCount; + console.log(`\n${bad ? '❌' : '✅'} Final Result:\n ${totalErrorCount} errors\n ${totalWarningCount} warnings`); + console.groupEnd(); + if (bad) { + process.exit(1); + } } async function lintFile(fpath, startRegex, endRegex) { @@ -161,15 +177,19 @@ async function lintFile(fpath, startRegex, endRegex) { const lineNumbers = findMatches(replayText, startRegex); const endLineNumbers = findMatches(replayText, endRegex); if (lineNumbers?.length != endLineNumbers?.length) { - throw new Error(`Lint failed in ${fpath} - start and end line numbers don't match: ${lineNumbers?.length} != ${endLineNumbers?.length}`); + throw new Error( + `Lint failed in ${fpath} - start and end line numbers don't match: ${lineNumbers?.length} != ${endLineNumbers?.length}` + ); } // console.log("lintFile", lineNumbers?.length, endLineNumbers?.length); jsTextBlocks = lineNumbers.map((lineNumber, index) => - extractNamedScriptBlock(replayText, lineNumber, endLineNumbers[index]), + extractNamedScriptBlock(replayText, lineNumber, endLineNumbers[index]) ); if (!jsTextBlocks.length) { - throw new Error(`Invalid regexes or file path. Could not find js text block in ${fpath}.`); + throw new Error( + `Invalid regexes or file path. Could not find js text block in ${fpath}.` + ); } } else { jsTextBlocks = [{ text: replayText }]; @@ -185,11 +205,10 @@ async function lintFile(fpath, startRegex, endRegex) { warningCount += blockWarningCount; } - console.log(`Total counts: ${errorCount} errors, ${warningCount} warnings`); + console.log(`Stats: ${errorCount} errors, ${warningCount} warnings`); + totalErrorCount += errorCount; + totalWarningCount += warningCount; console.groupEnd(); - if (errorCount > 0) { - process.exit(1); - } } main(); diff --git a/replay_build_scripts/upload_build_artifacts.mjs b/replay_build_scripts/upload_build_artifacts.mjs index baf77652968274..01478512894773 100644 --- a/replay_build_scripts/upload_build_artifacts.mjs +++ b/replay_build_scripts/upload_build_artifacts.mjs @@ -172,17 +172,18 @@ function prepareMacOSBinaries(buildId) { ? `${buildId}-arm-signed.dmg` : `${buildId}-signed.dmg`; const outdir = buildArm ? "out/Release-ARM" : "out/Release"; - const appPath = path.join(outdir, "Replay-Chromium.app"); + const appDstName = "Replay-Chromium.app"; + const appDstPath = path.join(outdir, appDstName); // Copy assets. copyAssets(path.join(outdir, "Chromium.app")); // Clean up. - fs.rmSync(appPath, { + fs.rmSync(appDstPath, { recursive: true, force: true, }); - fs.renameSync(path.join(outdir, "Chromium.app"), appPath); + fs.renameSync(path.join(outdir, "Chromium.app"), appDstPath); // Bundle dmg file. spawnChecked( @@ -196,7 +197,7 @@ function prepareMacOSBinaries(buildId) { "-fs", "HFS+", "-srcfolder", - "Replay-Chromium.app", + appDstName, ], { cwd: outdir, stdio: "inherit" } ); @@ -211,7 +212,7 @@ function prepareMacOSBinaries(buildId) { // Bundle tar ball. spawnChecked( "tar", - ["cfJ", path.join(process.cwd(), buildIdTarArchive), "Replay-Chromium.app"], + ["cfJ", path.join(process.cwd(), buildIdTarArchive), appDstName], { cwd: outdir } ); @@ -280,7 +281,7 @@ function prepareMacOSBinaries(buildId) { "--p12-password-file", p12PassPath, ...codeSignatureFlags, - appPath, + appDstPath, ], { stdio: "inherit" } ); @@ -295,7 +296,7 @@ function prepareMacOSBinaries(buildId) { "-fs", "HFS+", "-srcfolder", - "Replay-Chromium.app", + appDstName, ], { cwd: outdir, stdio: "inherit" } ); @@ -327,7 +328,7 @@ function prepareMacOSBinaries(buildId) { } // Clean up. - fs.renameSync(appPath, path.join(outdir, "Chromium.app")); + fs.renameSync(appDstPath, path.join(outdir, "Chromium.app")); // Move things into place. fs.cpSync(buildIdDmgArchive, dmgArchive); diff --git a/third_party/blink/renderer/bindings/core/v8/local_window_proxy.cc b/third_party/blink/renderer/bindings/core/v8/local_window_proxy.cc index 975d67e33cc55a..e619e337ad8bfe 100644 --- a/third_party/blink/renderer/bindings/core/v8/local_window_proxy.cc +++ b/third_party/blink/renderer/bindings/core/v8/local_window_proxy.cc @@ -185,7 +185,6 @@ static bool gRecordReplayStateInitialized; void LocalWindowProxy::Initialize() { // https://linear.app/replay/issue/RUN-749 recordreplay::Assert("LocalWindowProxy::Initialize Start"); - recordreplay::AutoMarkerDependencyExecution execute( "ScriptExecution", "LocalWindowProxy::Initialize" ); @@ -240,56 +239,62 @@ void LocalWindowProxy::Initialize() { SetSecurityToken(origin.get()); } - if (recordreplay::IsRecordingOrReplaying("commands") && - origin && !origin->Host().empty()) { - bool initGlobally = !gRecordReplayStateInitialized; - - // Whether this is the relative root frame of this process. - bool isMainFrame = GetFrame()->IsMainFrame() && world_->IsMainWorld(); - if (initGlobally) { - gRecordReplayStateInitialized = true; - - if (!isMainFrame) { - recordreplay::Warning( - "LocalWindowProxy::Initialize Called on non-root frame first: %d %d origin=%s url=%s", - GetFrame()->IsMainFrame(), - world_->IsMainWorld(), - origin->ToRawString().Utf8().c_str(), - GetFrame()->GetDocument()->Url().GetString().Utf8().c_str()); + { + recordreplay::AutoMarkReplayCode amrc; + + if (recordreplay::IsRecordingOrReplaying("commands") && + origin && !origin->Host().empty() && + world_->IsMainWorld()) { + + bool initGlobally = !gRecordReplayStateInitialized; + + // Whether this is the relative root frame of this process. + bool isLocalRoot = GetFrame()->IsLocalRoot(); + if (initGlobally) { + gRecordReplayStateInitialized = true; + + if (!isLocalRoot) { + recordreplay::Warning( + "LocalWindowProxy::Initialize Called on frame that does not OnRootFrameInit: %d %d origin=%s url=%s", + GetFrame()->IsMainFrame(), + world_->IsMainWorld(), + origin->ToRawString().Utf8().c_str(), + GetFrame()->GetDocument()->Url().GetString().Utf8().c_str()); + } + + // After creating the first context that is associated with a non-empty + // origin, we are ready to set up the state used to process driver + // commands when recording/replaying, and to create checkpoints. + InitializeRecordReplay( + RecordReplayGetProcessType( + GetFrame(), + world_ + ), + GetIsolate(), GetFrame(), context + ); } - // After creating the first context that is associated with a non-empty - // origin, we are ready to set up the state used to process driver - // commands when recording/replaying, and to create checkpoints. - InitializeRecordReplay( - RecordReplayGetProcessType( - GetFrame(), - world_ - ), - GetIsolate(), GetFrame(), context - ); - } + if (isLocalRoot) { + // Root-level navigation event, initially happens before + // first checkpoint. + OnRootFrameInit(GetIsolate(), GetFrame(), context); + } - if (isMainFrame) { - // Root-level navigation event, initially happens before - // first checkpoint. - OnRootFrameInit(GetIsolate(), GetFrame(), context); - } + if (initGlobally) { + // Create the first checkpoint at which execution can pause. + recordreplay::NewCheckpoint(); + // Initialize some more. + InitializeRecordReplayAfterCheckpoint(); + } + + if (isLocalRoot) { + // Root-level navigation event, after first checkpoint. + OnRootFrameInitAfterCheckpoint(GetIsolate(), GetFrame(), context); + } - if (initGlobally) { - // Create the first checkpoint at which execution can pause. - recordreplay::NewCheckpoint(); - // Initialize some more. - InitializeRecordReplayAfterCheckpoint(); + // Event for all new windows. + OnNewWindowAfterCheckpoint(GetFrame(), context); } - - if (isMainFrame) { - // Root-level navigation event, after first checkpoint. - OnRootFrameInitAfterCheckpoint(GetIsolate(), GetFrame(), context); - } - - // Event for all new windows. - OnNewWindowAfterCheckpoint(GetIsolate(), GetFrame(), context); } { diff --git a/third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc b/third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc index d6fa6c39f9aadc..9be404f5c34f28 100644 --- a/third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc +++ b/third_party/blink/renderer/bindings/core/v8/record_replay_interface.cc @@ -13,6 +13,7 @@ #include "base/base64.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" +#include "base/path_service.h" #include "base/process/process_handle.h" #include "base/record_replay.h" #include "base/record_replay_paint_surface.h" @@ -42,6 +43,7 @@ #include "third_party/blink/renderer/platform/bindings/v8_dom_wrapper.h" #include "third_party/inspector_protocol/crdtp/maybe.h" #include "v8/include/v8-inspector.h" +#include "v8/include/replayio.h" #include #include @@ -58,14 +60,8 @@ static const char DirectorySeparator = '\\'; static const char *AnnotationHookJSName = "__RECORD_REPLAY_ANNOTATION_HOOK__"; namespace v8 { - -extern void FunctionCallbackRecordReplaySetCommandCallback(const FunctionCallbackInfo& args); -extern void FunctionCallbackRecordReplaySetClearPauseDataCallback(const FunctionCallbackInfo& callArgs); -extern void FunctionCallbackRecordReplayAddNewScriptHandler(const FunctionCallbackInfo& args); -extern void FunctionCallbackRecordReplayGetScriptSource(const FunctionCallbackInfo& args); - namespace internal { - +extern void FunctionCallbackRecordReplayGetScriptSource(const FunctionCallbackInfo& args); extern int RecordReplayObjectId(v8::Isolate* isolate, v8::Local cx, v8::Local object, bool allow_create); extern void RecordReplayConfirmObjectHasId(v8::Isolate* isolate, @@ -89,9 +85,7 @@ using RemoteObjectIdTypeRaw = std::u16string; // The more convenient type that we use using RemoteObjectIdType = WTF::String; -extern "C" void V8RecordReplaySetDefaultContext(v8::Isolate* isolate, v8::Local cx); extern "C" void V8RecordReplayFinishRecording(); -extern "C" void V8RecordReplaySetCrashReason(const char* reason); extern "C" char* V8RecordReplayReadAssetFileContents(const char* aPath, size_t* aLength); extern "C" void V8RecordReplayOnConsoleMessage(size_t bookmark); extern "C" void V8RecordReplayAddMetadata(const char* jsonString); @@ -113,24 +107,24 @@ static bool IsCommandHandlingEnabled() { IsCommandHandlingEnabledWhenRecording(); } -static LocalFrame* GetLocalFrameRoot(v8::Isolate* isolate) { +static LocalFrame* GetCurrentLocalFrameRoot(v8::Isolate* isolate) { LocalDOMWindow* currentWindow = CurrentDOMWindow(isolate); if (!currentWindow) { - recordreplay::Print("[RuntimeError] GetLocalFrameRoot: no window."); + recordreplay::Print("[RuntimeError] GetCurrentLocalFrameRoot: no window."); return nullptr; } LocalFrame *f = currentWindow->GetFrame(); if (!f || f->IsDetached() || f->IsProvisional()) { - recordreplay::Print("[RuntimeError] GetLocalFrameRoot: window has no frame."); + recordreplay::Print("[RuntimeError] GetCurrentLocalFrameRoot: window has no frame."); return nullptr; } LocalFrame& root = f->LocalFrameRoot(); if (root.IsDetached() || root.IsProvisional()) { - recordreplay::Print("[RuntimeError] GetLocalFrameRoot: root is detached or provisional."); + recordreplay::Print("[RuntimeError] GetCurrentLocalFrameRoot: root is detached or provisional."); return nullptr; } @@ -165,7 +159,7 @@ class InspectorData { inspectorSession = nullptr; } - LocalFrame* GetLocalFrameRoot() const { return blink::GetLocalFrameRoot(isolate); } + LocalFrame* GetCurrentLocalFrameRoot() const { return blink::GetCurrentLocalFrameRoot(isolate); } }; static LocalFrame* gRootLocalFrame = nullptr; @@ -175,20 +169,41 @@ typedef std::unordered_map ContextGroupIdInspectorMap; std::unordered_map* gInspectorData = nullptr; std::unordered_map* gV8Inspectors = nullptr; -static std::string ReadReplayAssetFileRaw(const char* filename, size_t& len) { - const char* scriptDir = getenv("RECORD_REPLAY_ASSETS_DIRECTORY"); - if (!scriptDir) { - recordreplay::Crash("ReadReplayAssetFileRaw failed: RECORD_REPLAY_ASSETS_DIRECTORY not provided"); +static std::string ReadReplayAssetFile(const char* filename, size_t& len) { + // TODO: Get "binary dir" from Chromium. + base::FilePath binPath; + std::string assetsDir; + + // TODO: Test this on mac. + if (getenv("RECORD_REPLAY_ASSETS_DIRECTORY")) { + assetsDir = getenv("RECORD_REPLAY_ASSETS_DIRECTORY"); + } else if (base::PathService::Get(base::FILE_EXE, &binPath)) { + // PathService::Get(base::DIR_EXE, result); + auto assetRoot = binPath.DirName(); +#if BUILDFLAG(IS_MAC) + assetRoot = assetRoot.AppendASCII("../../../../../../../../.."); +#endif + assetsDir = assetRoot.AppendASCII("replay-assets").AsUTF8Unsafe(); } - - std::string fpath = std::string(scriptDir) + std::string("/") + filename; + if (!assetsDir.length()) { + recordreplay::Crash("ReadReplayAssetFile failed: Neither base::FILE_EXE nor RECORD_REPLAY_ASSETS_DIRECTORY provided."); + } + // TODO: We have to go up by 9 directories on Mac - + // E.g.: + // Replay-Chromium.app/Contents/Frameworks/Chromium Framework.framework/Versions/108.0.5359.0/Helpers/Chromium Helper (Renderer).app/Contents/MacOS/replay-assets/replay_sourcemap_handler.js + std::string fpath = assetsDir + std::string("/") + filename; std::ifstream ifs(fpath); std::stringstream ss; ss << ifs.rdbuf(); std::string s = ss.str(); len = s.length(); + recordreplay::Print("ReadReplayAssetFile from '%s' (%zu)", + fpath.c_str(), + len); if (!len) { - recordreplay::Crash("ReadReplayAssetFileRaw failed: %s", fpath.c_str()); + recordreplay::Crash("ReadReplayAssetFile (\"%s\") failed: %s", + fpath.c_str(), + strerror(errno)); } return s; } @@ -196,11 +211,22 @@ static std::string ReadReplayAssetFileRaw(const char* filename, size_t& len) { static String ReadReplayAssetFile(const char* fname) { size_t len; + // Important: Treat as UTF-8. + String result = String::FromUTF8(ReadReplayAssetFile(fname, len).c_str(), len); + if (!len) { + recordreplay::Crash("ReadReplayAssetFile failed: %s", fname); + } + return result; +} + +static String ReadReplayCommandAssetFile(const char* fname) { + size_t len; + // Important: Treat as UTF-8. String result = String::FromUTF8( IsCommandHandlingEnabledWhenRecording() // Recording + Replay. - ? ReadReplayAssetFileRaw(fname, len).c_str() + ? ReadReplayAssetFile(fname, len).c_str() // Replay only. : V8RecordReplayReadAssetFileContents(fname, &len), len @@ -211,268 +237,22 @@ static String ReadReplayAssetFile(const char* fname) { return result; } -// static -String ReadReplayCommandHandlerScript() { - return ReadReplayAssetFile("replay_command_handlers.js"); +static String ReadReplayJsEventsBaseScript() { + return ReadReplayAssetFile("replay_init.js"); } - -/** ########################################################################### - * gSourceMapScript - * ##########################################################################*/ - -// Script which sets a handler for collecting source maps from scripts in the -// recording. Runs when recording/replaying if source map collection is enabled. -const char* gSourceMapScript = R""""( -//js -(() => { - -// Avoid monkey patching. -const fetch = window.fetch; -const DateNow = Date.now; - -const { - log, - warning, - getRecordingId, - sha256DigestHex, - writeToRecordingDirectory, - addRecordingEvent, - addNewScriptHandler, - getScriptSource, - recordingDirectoryFileExists, - readFromRecordingDirectory, - getRecordingFilePath, - RECORD_REPLAY_DISABLE_SOURCEMAP_CACHE, -} = __RECORD_REPLAY_ARGUMENTS__; - -const cache = {}; - -// Provide a cache for urls, salted with the supplied hash. Practically, this -// means if the script content changes at the url, we will re-download the resource. -async function getCachedResource(url, hash) { - const key = `${url}:${hash}`; - if (cache[key] && !RECORD_REPLAY_DISABLE_SOURCEMAP_CACHE) { - return cache[key]; - } - - log(`fetching sourcemap resource ${key}`); - - const res = await fetchText(url); - cache[key] = res; - return res; -} - -addNewScriptHandler(async (scriptId, sourceURL, relativeSourceMapURL) => { - try { - if (!relativeSourceMapURL || relativeSourceMapURL.startsWith("data:")) - return; - - const recordingId = getRecordingId(); - if (!recordingId) { - // The recording has been invalidated. - return; - } - - const urls = getSourceMapURLs(sourceURL, relativeSourceMapURL); - if (!urls) - return; - - const scriptSource = getScriptSource(scriptId); - const scriptHash = sha256DigestHex(scriptSource); - - const { sourceMapURL, sourceMapBaseURL } = urls; - - let sourceMap; - try { - sourceMap = await getCachedResource(sourceMapURL, scriptHash); - } catch (err) { - log(`[RuntimeError] Failed to read sourcemap ${sourceMapURL}: ${err.message}`); - } - if (!sourceMap) { - return; - } - - const id = scriptHash; - const name = `sourcemap-${id}.map`; - const lookupName = `sourcemap-${id}.lookup`; - - let sources; - if (recordingDirectoryFileExists(name) && recordingDirectoryFileExists(lookupName)) { - try { - sources = JSON.parse(readFromRecordingDirectory(lookupName)); - } catch (err) { - log(`[RuntimeError][sourcemaps] Failed to load sourcemaps from file: ${lookupName} - ${err.message}`); - } - } - - if (!sources) { - writeToRecordingDirectory(name, sourceMap); - - sources = collectUnresolvedSourceMapResources(sourceMap, sourceMapURL, sourceURL); - writeToRecordingDirectory(lookupName, JSON.stringify(sources)); - } - - addRecordingEvent(JSON.stringify({ - kind: "sourcemapAdded", - path: getRecordingFilePath(name), - recordingId, - id, - url: sourceMapURL, - baseURL: sourceMapBaseURL, - targetContentHash: `sha256:${scriptHash}`, - targetURLHash: sourceURL ? makeAPIHash(sourceURL) : undefined, - targetMapURLHash: makeAPIHash(sourceMapURL), - timestamp: DateNow(), - })); - - for (const { offset, url } of sources) { - let sourceContent; - try { - sourceContent = await getCachedResource(url, scriptHash); - } catch (err) { - log(`[RuntimeError][sourcemaps] Failed to read original source ${url}: ${err.message}`); - continue; - } - const hash = sha256DigestHex(sourceContent); - const name = `source-${hash}`; - - if (!recordingDirectoryFileExists(name)) { - writeToRecordingDirectory(name, sourceContent); - } - addRecordingEvent(JSON.stringify({ - kind: "originalSourceAdded", - path: getRecordingFilePath(name), - recordingId, - parentId: id, - parentOffset: offset, - timestamp: DateNow(), - })); - } - } catch (err) { - warning(`[RuntimeError][sourcemaps] Exception - ${err?.stack || err}`); - } -}); - -async function fetchText(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Fetching ${url} failed with status code ${response.status} (${response.statusText})`); - } - return await response.text(); +static String ReadReplayCommandHandlerScript() { + return ReadReplayCommandAssetFile("replay_command_handlers.js"); } -function makeAPIHash(content) { - assert(typeof content === "string"); - const digestHex = sha256DigestHex(content); - return "sha256:" + digestHex; +static String ReadReplaySourcemapHandlerScript() { + return ReadReplayAssetFile("replay_sourcemap_handler.js"); } -function collectUnresolvedSourceMapResources(mapText, mapURL) { - let obj; - let sourceOffset = 0; - - function logError(msg) { - log(`[RuntimeError][sourcemaps] ${msg} (${mapURL}:${sourceOffset})`); - } - try { - obj = JSON.parse(mapText); - if (typeof obj !== "object" || !obj) { - return []; - } - } catch (err) { - logError(`Exception parsing sourcemap JSON (${mapURL}): ${err?.message || err}`); - return []; - } - - const unresolvedSources = []; - if (obj.version !== 3) { - logError("Invalid sourcemap version: " + obj.version); - return []; - } - - if (obj.sources != null) { - const { sourceRoot, sources, sourcesContent } = obj; - - if (Array.isArray(sources)) { - for (let i = 0; i < sources.length; i++) { - const offset = sourceOffset++; - - if ( - !Array.isArray(sourcesContent) || - typeof sourcesContent[i] !== "string" - ) { - let url = sources[i]; - if (typeof sourceRoot === "string" && sourceRoot) { - url = sourceRoot.replace(/\/?/, "/") + url; - } - let sourceURL; - try { - sourceURL = new URL(url, mapURL).toString(); - } catch { - logError("Unable to compute original source URL: " + url); - continue; - } - - unresolvedSources.push({ - offset, - url: sourceURL, - }); - } - } - } else { - logError("Invalid sourcemap sources list"); - } - } - - return unresolvedSources; -} - -function assert(v, msg = "") { - if (!v) { - const m = `Assertion failed when handling command (${msg})`; - log(`[RuntimeError] ${m} - ${Error().stack}`); - throw new Error(m); - } -} - -function getSourceMapURLs(sourceURL, relativeSourceMapURL) { - let sourceBaseURL; - if (typeof sourceURL === "string" && isValidBaseURL(sourceURL)) { - sourceBaseURL = sourceURL; - } else if (window?.location?.href && isValidBaseURL(window?.location?.href)) { - sourceBaseURL = window.location.href; - } - - let sourceMapURL; - try { - sourceMapURL = new URL(relativeSourceMapURL, sourceBaseURL).toString(); - } catch (err) { - log("Failed to process sourcemap url: " + err.message); - return null; - } - - // If the map was a data: URL or something along those lines, we want - // to resolve paths in the map relative to the overall base. - const sourceMapBaseURL = - isValidBaseURL(sourceMapURL) ? sourceMapURL : sourceBaseURL; - - return { sourceMapURL, sourceMapBaseURL }; -} - -function isValidBaseURL(url) { - try { - new URL("", url); - return true; - } catch { - return false; - } -} - -})(); - -)""""; +/** ########################################################################### + * RDT Scripts + * ##########################################################################*/ // Script that injects React DevTools "stub" functions to capture // marker annotations while recording, for use in later processing @@ -989,10 +769,11 @@ struct InspectorChannel final : public v8_inspector::V8Inspector::Channel { }; absl::optional GetCurrentContextGroupIdForIsolate(v8::Isolate* isolate) { - LocalFrame* local_frame_root = GetLocalFrameRoot(isolate); + LocalFrame* local_frame_root = GetCurrentLocalFrameRoot(isolate); - if (local_frame_root != nullptr) { - // Get (do NOT create) a ContextGroupId: + if (local_frame_root) { + // Gets (but does NOT create!) a contextGroupId. + // NOTE: Copied from |MainThreadDebugger::ContextGroupId|. return WeakIdentifierMap::Identifier(local_frame_root); } @@ -1386,7 +1167,7 @@ static InspectedFrames* getOrCreateInspectedFrames(v8::Isolate* isolate, int con InspectorData *data = getInspectorFor(isolate, contextGroupId); if (!data->inspectedFrames) { - data->inspectedFrames = MakeGarbageCollected(data->GetLocalFrameRoot()); + data->inspectedFrames = MakeGarbageCollected(data->GetCurrentLocalFrameRoot()); } return data->inspectedFrames; } @@ -1407,7 +1188,7 @@ absl::optional getOrCreateInspectorDOMAgent(v8::Isolate* iso InspectedFrames* inspectedFrames = getOrCreateInspectedFrames(isolate, *contextGroupId); data->inspectorDomAgent = MakeGarbageCollected( isolate, inspectedFrames, getInspectorSession(isolate, *contextGroupId)); - data->inspectorDomAgent->FrameDocumentUpdated(data->GetLocalFrameRoot()); + data->inspectorDomAgent->FrameDocumentUpdated(data->GetCurrentLocalFrameRoot()); } return data->inspectorDomAgent; } @@ -1429,7 +1210,7 @@ absl::optional getOrCreateInspectorDOMDebuggerAgent( // RUN-1061: registering the agent here allows it to receive `UserCallback` // events. - data->GetLocalFrameRoot()->GetProbeSink()->AddInspectorDOMDebuggerAgent(data->inspectorDomDebuggerAgent); + data->GetCurrentLocalFrameRoot()->GetProbeSink()->AddInspectorDOMDebuggerAgent(data->inspectorDomDebuggerAgent); } return data->inspectorDomDebuggerAgent; } @@ -1463,7 +1244,7 @@ absl::optional getOrCreateInspectorCSSAgent(v8::Isolate* iso InspectedFrames* inspectedFrames = getOrCreateInspectedFrames(isolate, *contextGroupId); auto* resource_content_loader = - MakeGarbageCollected(data->GetLocalFrameRoot()); + MakeGarbageCollected(data->GetCurrentLocalFrameRoot()); auto* resource_container = MakeGarbageCollected(inspectedFrames); auto domAgent = getOrCreateInspectorDOMAgent(isolate); @@ -2478,80 +2259,19 @@ static void InvokeOnAnnotation(const v8::FunctionCallbackInfo& args) recordreplay::OnAnnotation(*kind, *contents); } -/** - * Copied from gin/try_catch.h. - */ -static v8::Local GetSourceLine(v8::Isolate* isolate, - v8::Local message) { - auto maybe = message->GetSourceLine(isolate->GetCurrentContext()); - v8::Local source_line; - return maybe.ToLocal(&source_line) ? source_line : v8::String::Empty(isolate); -} - static const std::string V8ToString(v8::Isolate* isolate, v8::Local str) { v8::String::Utf8Value s(isolate, str); return *s; } -/** - * Error reporting utility based on ShellRunner::Run. - * WARNING: It does not work very well. For some reason, we have to try/catch - * inside the JS code to get a proper error message. Might have to do with the - * fact that we are running this before the window and/or other mechanisms have - * not fully initialized. - */ -static std::string GetStackTrace(v8::Isolate* isolate, v8::TryCatch& try_catch) { - if (!try_catch.HasCaught()) { - return ""; - } - - std::stringstream ss; - v8::Local message = try_catch.Message(); - if (!message.IsEmpty()) { - ss << V8ToString(isolate, message->Get()) << std::endl; - } - ss << V8ToString(isolate, GetSourceLine(isolate, message)) << std::endl; - - // v8::Local trace = message->GetStackTrace(); - // if (trace.IsEmpty()) - // return ss.str(); - - // int len = trace->GetFrameCount(); - // for (int i = 0; i < len; ++i) { - // v8::Local frame = trace->GetFrame(isolate, i); - // ss << V8ToString(isolate, frame->GetScriptName()) << ":" - // << frame->GetLineNumber() << ":" << frame->GetColumn() << ": " - // << V8ToString(isolate, frame->GetFunctionName()) << std::endl; - // } - return ss.str(); -} - -static void RunScript(v8::Isolate* isolate, v8::Local context, const char* source_raw, const char* filename) { - v8::Local filename_string = ToV8String(isolate, filename); - v8::ScriptOrigin origin(isolate, filename_string); - - v8::TryCatch try_catch(isolate); - v8::Local source = ToV8String(isolate, source_raw); - auto maybe_script = v8::Script::Compile(context, source, &origin); - - v8::Local script; - if (!maybe_script.ToLocal(&script)) { - recordreplay::Crash("Replay RunScript COMPILE failed: %s", - GetStackTrace(isolate, try_catch).c_str()); - } - v8::Local rv; - if (!script->Run(context).ToLocal(&rv)) { - recordreplay::Crash("Replay RunScript INIT failed: %s", - GetStackTrace(isolate, try_catch).c_str()); - } -} - static bool TestEnv(const char* env) { const char* v = getenv(env); return v && v[0] && v[0] != '0'; } -static void InitializeRecordReplayApiObjects(v8::Isolate* isolate, LocalFrame* localFrame) { +static void InitializeRecordReplayApiObjects( + v8::Isolate* isolate, LocalFrame* localFrame, v8::replayio::ReplayRootContext* newRoot + ) { v8::Local context = isolate->GetCurrentContext(); // Add __RECORD_REPLAY_ANNOTATION_HOOK__ as a global. @@ -2591,11 +2311,11 @@ static void InitializeRecordReplayApiObjects(v8::Isolate* isolate, LocalFrame* l SetFunctionProperty(isolate, args, "setCDPMessageCallback", SetCDPMessageCallback); SetFunctionProperty(isolate, args, "sendCDPMessage", SendCDPMessage); - SetFunctionProperty(isolate, args, "setCommandCallback", - v8::FunctionCallbackRecordReplaySetCommandCallback); SetFunctionProperty(isolate, args, "layoutDom", LayoutDom); + // DefineProperty(isolate, args, "ReplayJsEventEmitter", newRoot->GetEventEmitter()); + // Object Util SetFunctionProperty(isolate, args, "fromJsMakeDebuggeeValue", fromJsMakeDebuggeeValue); @@ -2647,19 +2367,14 @@ static void InitializeRecordReplayApiObjects(v8::Isolate* isolate, LocalFrame* l fromJsGetCurrentViewportPixelSize); // unsorted Replay stuff - SetFunctionProperty( - isolate, args, "setClearPauseDataCallback", - v8::FunctionCallbackRecordReplaySetClearPauseDataCallback); SetFunctionProperty(isolate, args, "getCurrentError", GetCurrentError); SetFunctionProperty(isolate, args, "getRecordingId", GetRecordingId); SetFunctionProperty(isolate, args, "sha256DigestHex", SHA256DigestHex); SetFunctionProperty(isolate, args, "writeToRecordingDirectory", WriteToRecordingDirectory); SetFunctionProperty(isolate, args, "addRecordingEvent", AddRecordingEvent); - SetFunctionProperty(isolate, args, "addNewScriptHandler", - v8::FunctionCallbackRecordReplayAddNewScriptHandler); SetFunctionProperty(isolate, args, "getScriptSource", - v8::FunctionCallbackRecordReplayGetScriptSource); + v8::internal::FunctionCallbackRecordReplayGetScriptSource); SetFunctionProperty(isolate, args, "recordingDirectoryFileExists", RecordingDirectoryFileExists); @@ -2695,24 +2410,19 @@ void InitializeRecordReplayAfterCheckpoint() { V8RecordReplayRegisterBrowserEventCallback(HandleBrowserEvent); } -static void InitializeReplayScripts(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local context) { +static const char* InternalScriptURL = "record-replay-internal"; + +static void InitializeRootContext(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local context) { // Register context, s.t. when handling a command and we are not on a // JS stack, we can always use the current root frame's context. // Note: We are assuming that each tab has its own process, for now. // (That might not hold true for tabs of the same domain - not sure) - V8RecordReplaySetDefaultContext(isolate, context); + v8::replayio::ReplayRootContext* newRoot = v8::replayio::RecordReplayCreateRootContext(isolate, context); // Initialize __RECORD_REPLAY__ things. - InitializeRecordReplayApiObjects(isolate, localFrame); + InitializeRecordReplayApiObjects(isolate, localFrame, newRoot); // This URL will prevent the script from being reported to the recorder. - const char* InternalScriptURL = "record-replay-internal"; - - if (recordreplay::FeatureEnabled("collect-source-maps") && - !TestEnv("RECORD_REPLAY_DISABLE_SOURCEMAP_COLLECTION")) { - recordreplay::AutoMarkReplayCode amrc; - RunScript(isolate, context, gSourceMapScript, InternalScriptURL); - } if (recordreplay::FeatureEnabled("force-main-world-initialization")) { // Call this here to avoid divergence later. @@ -2720,21 +2430,51 @@ static void InitializeReplayScripts(v8::Isolate* isolate, LocalFrame* localFrame localFrame->GetSettings()->SetForceMainWorldInitialization(true); } + { + newRoot->RunScriptAndCallBack( + ReadReplayJsEventsBaseScript().Utf8().c_str(), + InternalScriptURL + std::string("://Replay-Init") + ); + } + + { + newRoot->RunScriptAndCallBack( + ReadReplaySourcemapHandlerScript().Utf8().c_str(), + InternalScriptURL + std::string("://Sourcemap-Handler") + ); + } + if (IsCommandHandlingEnabled()) { - recordreplay::AutoMarkReplayCode amrc; String commandHandlerScript = ReadReplayCommandHandlerScript(); { - recordreplay::AutoDisallowEvents disallow("InitializeReplayScripts"); + recordreplay::AutoDisallowEvents disallow("InitializeRootContext"); // Run `commandHandlerScript`. - RunScript(isolate, context, commandHandlerScript.Utf8().c_str(), InternalScriptURL); + newRoot->RunScriptAndCallBack( + commandHandlerScript.Utf8().c_str(), + InternalScriptURL + std::string("://Command-Handler") + ); } } + + if (recordreplay::FeatureEnabled("collect-source-maps") && + !TestEnv("RECORD_REPLAY_DISABLE_SOURCEMAP_COLLECTION")) { + newRoot->RunScriptAndCallBack( + ReadReplaySourcemapHandlerScript().Utf8().c_str(), + InternalScriptURL + std::string("://SourcemapHandler") + ); + } } void OnRootFrameInit(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local context) { - recordreplay::AutoMarkReplayCode amrc; - recordreplay::Trace( + // TODO: keep track of this frame's and context's lifecycle? + // → Find all relevant events. + // → Log contextGroupId + contextId + // → See how contextGroupId in blink maps against contextGroupId in V8. It should be the same. Its also the V8InspectorSession id. + // → Consider using gContextChangeCallbacks for this. + // TODO: fix and use GetCurrentLocalFrameRoot + // TODO: consider using blink::GetCurrentLocalFrameRoot + recordreplay::Print( "[RUN-2739] OnRootFrameInit win=%d frame=%d %d %d %d %d parent=%d" " \"%s\"", localFrame->DomWindow()->RecordReplayId(), localFrame->RecordReplayId(), @@ -2748,6 +2488,8 @@ void OnRootFrameInit(v8::Isolate* isolate, LocalFrame* localFrame, v8::LocalGetDocument()->Url().GetString().Utf8().c_str() ); + + CHECK(localFrame == localFrame->LocalFrameRoot()); // NOTE: The root `LocalFrame` can change over time. gRootLocalFrame = localFrame; @@ -2759,7 +2501,7 @@ void OnRootFrameInit(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local context) { @@ -2775,20 +2517,20 @@ void OnRootFrameInitAfterCheckpoint(v8::Isolate* isolate, LocalFrame* localFrame // Note: We use a special URL for the react devtools as this script needs // to be reported to the recorder so that evaluations can be performed in // its frames. - RunScript(isolate, context, gReactDevtoolsScript, "record-replay-react-devtools"); - RunScript(isolate, context, gReduxDevtoolsScript, "record-replay-redux-devtools"); + v8::replayio::ReplayRootContext* root = v8::replayio::RecordReplayGetRootContext(context); + root->RunScriptAndCallBack(gReactDevtoolsScript, "record-replay-react-devtools"); + root->RunScriptAndCallBack(gReduxDevtoolsScript, "record-replay-redux-devtools"); } } -void OnNewWindowAfterCheckpoint(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local newContext) { - recordreplay::AutoMarkReplayCode amrc; - RunScript(isolate, newContext, gOnNewWindowScript, - "record-replay-OnNewWindow"); +void OnNewWindowAfterCheckpoint(LocalFrame* localFrame, v8::Local newContext) { + v8::replayio::ReplayRootContext* root = v8::replayio::RecordReplayGetRootContext(newContext); + root->RunScriptAndCallBack(gOnNewWindowScript, "record-replay-OnNewWindow"); LocalFrame* parentFrame = DynamicTo(localFrame->Parent()); recordreplay::Print( "[RUN-2739] OnNewWindowAfterCheckpoint %d win=%d frame=%d %d \"%s\" parent=%d", - newContext == isolate->GetCurrentContext(), + newContext == root->GetContext(), localFrame->DomWindow()->RecordReplayId(), localFrame->RecordReplayId(), localFrame->IsCrossOriginToParentOrOuterDocument(), diff --git a/third_party/blink/renderer/bindings/core/v8/record_replay_interface.h b/third_party/blink/renderer/bindings/core/v8/record_replay_interface.h index c76d2078e2b202..ae4bb9d0a56cd0 100644 --- a/third_party/blink/renderer/bindings/core/v8/record_replay_interface.h +++ b/third_party/blink/renderer/bindings/core/v8/record_replay_interface.h @@ -32,7 +32,7 @@ void OnRootFrameInitAfterCheckpoint(v8::Isolate* isolate, LocalFrame* localFrame // Initialize everything that depends on other initialization steps but // for all windows. // This is the last Replay code that we run for a new Window object. -void OnNewWindowAfterCheckpoint(v8::Isolate* isolate, LocalFrame* localFrame, v8::Local context); +void OnNewWindowAfterCheckpoint(LocalFrame* localFrame, v8::Local context); // Notify the driver that we're adding an error to the console. void RecordReplayOnErrorEvent(ErrorEvent* error_event); diff --git a/third_party/blink/renderer/bindings/scripts/bind_gen/interface.py b/third_party/blink/renderer/bindings/scripts/bind_gen/interface.py index e6b36d5e474631..b11c80284b14ea 100644 --- a/third_party/blink/renderer/bindings/scripts/bind_gen/interface.py +++ b/third_party/blink/renderer/bindings/scripts/bind_gen/interface.py @@ -337,9 +337,25 @@ def bind_callback_local_vars(code_node, cg_context): "V8PerIsolateData::From(${isolate});")), S("property_name", "const char* const ${property_name} = \"${property.identifier}\";"), + # S("receiver_context", + # ("v8::Local ${receiver_context} = " + # "${v8_receiver}->GetCreationContextChecked();")), S("receiver_context", - ("v8::Local ${receiver_context} = " - "${v8_receiver}->GetCreationContextChecked();")), + ("v8::Local ${receiver_context} = " + + "${v8_receiver}->GetCreationContextChecked();") + # TODO: Remove this once we feel safe that this recorder crash is gone. + if (cg_context.class_like.identifier != "Window" or + (cg_context.property_ and cg_context.property_.identifier) != "fetch") + else + ("""v8::Local ${receiver_context} = v8_receiver->GetCreationContext().FromMaybe(v8::Local()); + if (${receiver_context}.IsEmpty()) { + std::string stack; + recordreplay::GetCurrentJSStack(&stack); + recordreplay::Warning("[TT-957] Context gone: %s", stack.c_str()); + exception_state.ThrowTypeError("[TT-957] Context Gone"); + return; + }""") + ), S("receiver_script_state", ("ScriptState* ${receiver_script_state} = " "ScriptState::From(${receiver_context});")), diff --git a/third_party/blink/renderer/core/fetch/global_fetch.cc b/third_party/blink/renderer/core/fetch/global_fetch.cc index 34eabb0bd637d6..e870e8f82d024a 100644 --- a/third_party/blink/renderer/core/fetch/global_fetch.cc +++ b/third_party/blink/renderer/core/fetch/global_fetch.cc @@ -18,8 +18,6 @@ namespace blink { -extern bool LocalDOMWindowPointerIsValid(LocalDOMWindow* window); - namespace { void MeasureFetchProperties(ExecutionContext* execution_context, @@ -134,16 +132,6 @@ ScriptPromise GlobalFetch::fetch(ScriptState* script_state, const V8RequestInfo* input, const RequestInit* init, ExceptionState& exception_state) { - // Workaround for invalid window pointers being used when this function was called - // when no creation context is available for an unknown reason. - // - // See https://linear.app/replay/issue/TT-957 - if (!LocalDOMWindowPointerIsValid(&window)) { - recordreplay::Warning("[TT-957] GlobalFetch::fetch Context gone"); - exception_state.ThrowTypeError("Invalid receiver."); - return ScriptPromise(); - } - UseCounter::Count(window.GetExecutionContext(), WebFeature::kFetch); if (!window.GetFrame()) { exception_state.ThrowTypeError("The global scope is shutting down."); diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc index 362f0f12fedc8b..62946c32d78c2c 100644 --- a/third_party/blink/renderer/core/frame/local_dom_window.cc +++ b/third_party/blink/renderer/core/frame/local_dom_window.cc @@ -192,15 +192,6 @@ class LocalDOMWindow::NetworkStateObserver final online_observer_handle_; }; -static std::unordered_set* gValidDOMWindowPointers; - -// Workaround for invalid window pointers being used. -// -// See https://linear.app/replay/issue/TT-957 -bool LocalDOMWindowPointerIsValid(LocalDOMWindow* window) { - return gValidDOMWindowPointers && gValidDOMWindowPointers->find(window) != gValidDOMWindowPointers->end(); -} - LocalDOMWindow::LocalDOMWindow(LocalFrame& frame, WindowAgent* agent) : DOMWindow(frame), ExecutionContext(V8PerIsolateData::MainThreadIsolate(), agent), @@ -222,12 +213,7 @@ LocalDOMWindow::LocalDOMWindow(LocalFrame& frame, WindowAgent* agent) post_message_counter_(PostMessagePartition::kSameProcess), network_state_observer_(MakeGarbageCollected(this)), closewatcher_stack_( - MakeGarbageCollected(this)) { - CHECK(IsMainThread()); - if (!gValidDOMWindowPointers) - gValidDOMWindowPointers = new std::unordered_set(); - gValidDOMWindowPointers->insert(this); -} + MakeGarbageCollected(this)) {} void LocalDOMWindow::BindContentSecurityPolicy() { DCHECK(!GetContentSecurityPolicy()->IsBound()); @@ -908,11 +894,7 @@ void LocalDOMWindow::DispatchPopstateEvent( DispatchEvent(*PopStateEvent::Create(std::move(state_object), history()), "LocalDOMWindow::DispatchPopstateEvent"); } -LocalDOMWindow::~LocalDOMWindow() { - CHECK(IsMainThread()); - CHECK(gValidDOMWindowPointers); - gValidDOMWindowPointers->erase(this); -} +LocalDOMWindow::~LocalDOMWindow() = default; void LocalDOMWindow::Dispose() { BackForwardCacheBufferLimitTracker::Get()