From 8b40614eaf9e367248515c3383d2a8c324fb202b Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Sat, 6 Jun 2026 15:38:56 -0400 Subject: [PATCH 01/40] Replay port: new files + deletions + 56 clean-applied hooks (171 hunks) --- .github/CODEOWNERS | 253 ------------ .github/ISSUE_TEMPLATE/config.yml | 14 - .github/PULL_REQUEST_TEMPLATE.md | 41 -- .github/SUPPORT.md | 27 -- .github/actions/build-test-branch/action.yml | 6 + .github/actions/build-test-branch/main.js | 79 ++++ .github/actions/build-test/action.yml | 6 + .github/actions/build-test/main.js | 56 +++ .github/actions/release/action.yml | 6 + .github/actions/release/main.js | 30 ++ .github/actions/utils.js | 83 ++++ .github/approveman.yml | 5 + .github/replay-soc2.yml | 16 + .github/workflows/auto-start-ci.yml | 74 ---- .github/workflows/build-tarball.yml | 141 ------- .github/workflows/build-test-branch.yml | 36 ++ .github/workflows/build-test.yml | 32 ++ .github/workflows/close-stalled.yml | 36 -- .github/workflows/comment-labeled.yml | 55 --- .../commit-lint-problem-matcher.json | 13 - .github/workflows/commit-lint.yml | 35 -- .github/workflows/commit-queue.yml | 99 ----- .github/workflows/coverage-linux.yml | 92 ----- .github/workflows/coverage-windows.yml | 105 ----- .github/workflows/daily.yml | 32 -- .../workflows/find-inactive-collaborators.yml | 50 --- .github/workflows/find-inactive-tsc.yml | 62 --- .github/workflows/label-pr.yml | 18 - .github/workflows/license-builder.yml | 38 -- .github/workflows/linters.yml | 285 -------------- .github/workflows/release.yml | 22 ++ .../remark-lint-problem-matcher.json | 22 -- .github/workflows/test-internet.yml | 72 ---- .github/workflows/test-linux.yml | 100 ----- .github/workflows/test-macos.yml | 142 ------- .github/workflows/tools.yml | 346 ----------------- .gitignore | 5 + REPLAY_BACKEND_REV | 1 + build.js | 146 +++++++ deps/icu-small/source/common/umutex.cpp | 12 +- deps/openssl/openssl/crypto/o_str.c | 4 + deps/openssl/openssl/crypto/rand/rand_lib.c | 10 + deps/uv/include/uv.h | 1 + deps/uv/include/uv/unix.h | 1 + deps/uv/src/threadpool.c | 5 + deps/uv/src/unix/darwin.c | 20 +- deps/uv/src/unix/kqueue.c | 4 + deps/uv/src/unix/loop.c | 1 + deps/uv/src/unix/process.c | 20 +- deps/uv/src/unix/proctitle.c | 5 + deps/uv/src/unix/signal.c | 34 ++ deps/uv/src/unix/stream.c | 10 + deps/uv/src/unix/thread.c | 8 + lib/internal/bootstrap/node.js | 44 +++ lib/internal/console/constructor.js | 16 +- lib/internal/process/pre_execution.js | 3 + lib/internal/recordreplay/main.js | 304 +++++++++++++++ lib/internal/recordreplay/message.js | 50 +++ lib/internal/recordreplay/object.js | 102 +++++ lib/internal/recordreplay/preview.js | 359 ++++++++++++++++++ lib/internal/recordreplay/sourcemap.js | 232 +++++++++++ lib/internal/recordreplay/utils.js | 13 + node.gyp | 1 + src/api/callback.cc | 2 + src/api/environment.cc | 7 +- src/async_wrap.cc | 12 + src/cares_wrap.cc | 6 + src/connection_wrap.cc | 3 +- src/crypto/crypto_context.cc | 5 + src/env.cc | 32 +- src/env.h | 2 + src/handle_wrap.cc | 16 +- src/inspector/main_thread_interface.cc | 10 + src/inspector/node_string.cc | 10 + src/inspector/worker_inspector.cc | 10 + src/inspector_agent.cc | 1 + src/node.cc | 218 ++++++++++- src/node.h | 14 + src/node_api.cc | 38 +- src/node_binding.cc | 22 +- src/node_buffer.cc | 7 + src/node_builtins.cc | 6 +- src/node_dir.cc | 7 + src/node_errors.cc | 15 + src/node_errors.h | 2 + src/node_file.cc | 7 + src/node_http_parser.cc | 3 + src/node_main_instance.cc | 3 + src/node_messaging.cc | 2 +- src/node_mutex.h | 16 +- src/node_options.cc | 3 + src/node_options.h | 1 + src/node_os.cc | 46 +++ src/node_process_methods.cc | 159 ++++++++ src/node_v8.cc | 17 +- src/node_worker.cc | 14 +- src/stream_base.cc | 4 + src/stream_pipe.cc | 2 + src/tracing/agent.cc | 4 + src/udp_wrap.cc | 1 + test/parallel/test-bootstrap-modules.js | 5 + 101 files changed, 2482 insertions(+), 2190 deletions(-) delete mode 100644 .github/CODEOWNERS delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/SUPPORT.md create mode 100644 .github/actions/build-test-branch/action.yml create mode 100644 .github/actions/build-test-branch/main.js create mode 100644 .github/actions/build-test/action.yml create mode 100644 .github/actions/build-test/main.js create mode 100644 .github/actions/release/action.yml create mode 100644 .github/actions/release/main.js create mode 100644 .github/actions/utils.js create mode 100644 .github/approveman.yml create mode 100644 .github/replay-soc2.yml delete mode 100644 .github/workflows/auto-start-ci.yml delete mode 100644 .github/workflows/build-tarball.yml create mode 100644 .github/workflows/build-test-branch.yml create mode 100644 .github/workflows/build-test.yml delete mode 100644 .github/workflows/close-stalled.yml delete mode 100644 .github/workflows/comment-labeled.yml delete mode 100644 .github/workflows/commit-lint-problem-matcher.json delete mode 100644 .github/workflows/commit-lint.yml delete mode 100644 .github/workflows/commit-queue.yml delete mode 100644 .github/workflows/coverage-linux.yml delete mode 100644 .github/workflows/coverage-windows.yml delete mode 100644 .github/workflows/daily.yml delete mode 100644 .github/workflows/find-inactive-collaborators.yml delete mode 100644 .github/workflows/find-inactive-tsc.yml delete mode 100644 .github/workflows/label-pr.yml delete mode 100644 .github/workflows/license-builder.yml delete mode 100644 .github/workflows/linters.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .github/workflows/remark-lint-problem-matcher.json delete mode 100644 .github/workflows/test-internet.yml delete mode 100644 .github/workflows/test-linux.yml delete mode 100644 .github/workflows/test-macos.yml delete mode 100644 .github/workflows/tools.yml create mode 100644 REPLAY_BACKEND_REV create mode 100644 build.js create mode 100644 lib/internal/recordreplay/main.js create mode 100644 lib/internal/recordreplay/message.js create mode 100644 lib/internal/recordreplay/object.js create mode 100644 lib/internal/recordreplay/preview.js create mode 100644 lib/internal/recordreplay/sourcemap.js create mode 100644 lib/internal/recordreplay/utils.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 22fef9e8c220..000000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,253 +0,0 @@ -# Node.js Project Codeowners - -# This file does not define any requirements for landing PRs. -# Its purpose is to allow automation of courtesy pings to the -# relevant team(s) when any of the paths listed here are modified. -# Criteria for landing PRs are defined in -# https://github.com/nodejs/node/blob/main/doc/contributing/collaborator-guide.md#code-reviews. -# -# Codeowners must always be teams, never individuals. - -# tsc - -/.github/CODEOWNERS @nodejs/tsc -/.github/PULL_REQUEST_TEMPLATE.md @nodejs/tsc -/.github/ISSUE_TEMPLATE/* @nodejs/tsc -/CODE_OF_CONDUCT.md @nodejs/tsc -/CONTRIBUTING.md @nodejs/tsc -/doc/contributing/**/* @nodejs/tsc -/GOVERNANCE.md @nodejs/tsc -/SECURITY.md @nodejs/tsc -/BUILDING.md @nodejs/build @nodejs/tsc -/LICENSE @nodejs/tsc -/onboarding.md @nodejs/tsc - -# nodejs.org website -/tools/doc @nodejs/web-infra - -# streams - -/lib/internal/streams/* @nodejs/streams -/lib/stream.js @nodejs/streams -/lib/stream/* @nodejs/streams - -# net - -/deps/cares @nodejs/net -/doc/api/dgram.md @nodejs/net -/doc/api/dns.md @nodejs/net -/doc/api/net.md @nodejs/net -/lib/dgram.js @nodejs/net -/lib/dns.js @nodejs/net -/lib/internal/dgram.js @nodejs/net -/lib/internal/dns/* @nodejs/net -/lib/internal/js_stream_socket.js @nodejs/net -/lib/internal/net.js @nodejs/net -/lib/internal/socket_list.js @nodejs/net -/lib/net.js @nodejs/net -/src/cares_wrap.cc @nodejs/net -/src/connect_wrap.* @nodejs/net -/src/connection_wrap.* @nodejs/net -/src/node_sockaddr* @nodejs/net -/src/tcp_wrap.* @nodejs/net -/src/udp_wrap.* @nodejs/net - -# tls/crypto - -/doc/api/crypto.md @nodejs/crypto -/doc/api/webcrypto.md @nodejs/crypto -/lib/crypto.js @nodejs/crypto -/lib/internal/crypto/* @nodejs/crypto -/lib/internal/tls/* @nodejs/crypto @nodejs/net -/lib/tls.js @nodejs/crypto @nodejs/net -/src/crypto/* @nodejs/crypto -/src/node_crypto* @nodejs/crypto -/deps/ncrypto/* @nodejs/crypto - -# http - -/deps/llhttp/* @nodejs/http @nodejs/net -/doc/api/http.md @nodejs/http @nodejs/net -/lib/_http_* @nodejs/http @nodejs/net -/lib/http.js @nodejs/http @nodejs/net -/lib/https.js @nodejs/crypto @nodejs/net @nodejs/http -/src/node_http_common* @nodejs/http @nodejs/http2 @nodejs/net -/src/node_http_parser.cc @nodejs/http @nodejs/net - -# http2 - -/deps/nghttp2/* @nodejs/http2 @nodejs/net -/doc/api/http2.md @nodejs/http2 @nodejs/http @nodejs/net -/lib/http2.js @nodejs/http2 @nodejs/net -/lib/internal/http2/* @nodejs/http2 @nodejs/net -/src/node_http2* @nodejs/http2 @nodejs/net -/src/node_mem* @nodejs/http2 - -# quic - -/deps/ngtcp2/* @nodejs/quic -/doc/api/quic.md @nodejs/quic -/lib/quic.js @nodejs/quic -/lib/internal/quic/* @nodejs/quic -/src/quic/* @nodejs/quic -/test/parallel/test-quic-* @nodejs/quic - -# modules, including loaders - -/doc/api/esm.md @nodejs/loaders -/doc/api/module.md @nodejs/loaders -/doc/api/modules.md @nodejs/loaders -/doc/api/packages.md @nodejs/loaders -/lib/internal/bootstrap/realm.js @nodejs/loaders -/lib/internal/modules/* @nodejs/loaders -/lib/internal/process/execution.js @nodejs/loaders -/lib/module.js @nodejs/loaders -/src/module_wrap* @nodejs/loaders @nodejs/vm - -# Node-API - -/doc/api/n-api.md @nodejs/node-api -/doc/contributing/adding-new-napi-api.md @nodejs/node-api -/src/js_native_api* @nodejs/node-api -/src/node_api* @nodejs/node-api - -# gyp - -*.gyp @nodejs/gyp -*.gypi @nodejs/gyp -/tools/gyp/**/* @nodejs/gyp - -# WASI - -/deps/uvwasi/ @nodejs/wasi -/doc/api/wasi.md @nodejs/wasi -/lib/wasi.js @nodejs/wasi -/src/node_wasi* @nodejs/wasi -/test/fixtures/wasi/ @nodejs/wasi -/test/wasi/ @nodejs/wasi - -# Startup - -/benchmark/misc/startup-* @nodejs/startup -/lib/internal/bootstrap/* @nodejs/startup -/src/node_builtins* @nodejs/startup -/src/node_realm* @nodejs/startup @nodejs/realm -/src/node_snapshot* @nodejs/startup -/src/node.cc @nodejs/startup -/test/parallel/test-bootstrap-* @nodejs/startup -/test/parallel/test-snapshot-* @nodejs/startup -/tools/snapshot/* @nodejs/startup - -# V8 - -/deps/v8/* @nodejs/v8-update -/tools/v8_gypfiles/* @nodejs/v8-update - -# Actions - -/.github/workflows/* @nodejs/actions -/.github/workflows/create-release-proposal.yml @nodejs/releasers -/tools/actions/* @nodejs/actions - -# Test runner - -/doc/api/test.md @nodejs/test_runner -/lib/internal/main/test_runner.js @nodejs/test_runner -/lib/internal/test_runner/* @nodejs/test_runner -/lib/test.js @nodejs/test_runner -/lib/test/reporters.js @nodejs/test_runner -/test/parallel/test-runner-* @nodejs/test_runner - -# Single Executable Applications -/deps/LIEF @nodejs/single-executable -/deps/postject @nodejs/single-executable -/doc/api/single-executable-applications.md @nodejs/single-executable -/doc/contributing/maintaining/maintaining-single-executable-application-support.md @nodejs/single-executable -/src/node_sea* @nodejs/single-executable -/test/fixtures/postject-copy @nodejs/single-executable -/test/sea @nodejs/single-executable -/tools/dep_updaters/update-postject.sh @nodejs/single-executable -/tools/dep_updaters/update-lief.sh @nodejs/single-executable - -# Permission Model -/doc/api/permissions.md @nodejs/security-wg -/lib/internal/process/permission.js @nodejs/security-wg -/src/permission/* @nodejs/security-wg -/test/parallel/test-permission-* @nodejs/security-wg - -# Security Release -/doc/contributing/security-release-process.md @nodejs/security-stewards - -# Dependency Update Tools - -/.github/workflows/tools.yml @nodejs/security-wg -/.github/workflows/update-openssl.yml @nodejs/security-wg -/.github/workflows/update-v8.yml @nodejs/security-wg @nodejs/v8-update -/deps @nodejs/security-wg -/tools/dep_updaters/* @nodejs/security-wg - -# Web Standards -/lib/internal/bootstrap/web/* @nodejs/web-standards -/lib/internal/navigator.js @nodejs/web-standards -/test/fixtures/wpt/ @nodejs/web-standards -/test/wpt/ @nodejs/web-standards - -# TypeScript - -/deps/amaro/ @nodejs/typescript -/doc/api/typescript.md @nodejs/typescript -/test/fixtures/typescript/ @nodejs/typescript -/tools/dep_updaters/update-amaro.sh @nodejs/typescript - -# Performance -/benchmark/* @nodejs/performance - -# URL -/deps/ada @nodejs/url -/lib/internal/url.js @nodejs/url -/lib/url.js @nodejs/url -/src/node_url.* @nodejs/url -/test/fixtures/wpt/url @nodejs/url - -# SQLite -/deps/sqlite/ @nodejs/sqlite -/doc/api/sqlite.md @nodejs/sqlite -/src/node_sqlite.* @nodejs/sqlite -/test/parallel/test-sqlite* @nodejs/sqlite -/test/sqlite/ @nodejs/sqlite -/tools/dep_updaters/update-sqlite.sh @nodejs/sqlite - -# Config -/doc/node-config-schema.json @nodejs/config -/src/node_config.* @nodejs/config -/src/node_dotenv.* @nodejs/config -/src/node_options.* @nodejs/config -/test/parallel/test-config-* @nodejs/config -/test/parallel/test-dotenv-* @nodejs/config - -# Inspector -/src/inspector/* @nodejs/inspector -/src/inspector_* @nodejs/inspector -/lib/internal/inspector/* @nodejs/inspector -/lib/internal/inspector_* @nodejs/inspector -/lib/inspector.js @nodejs/inspector - -# path -/lib/path.js @nodejs/path -/lib/path/* @nodejs/path -/test/parallel/test-path-* @nodejs/path - -# userland-migrations -/doc/api/deprecations.md @nodejs/userland-migrations - -# dev container -/.devcontainer/* @nodejs/devcontainer -/doc/contributing/using-devcontainer.md @nodejs/devcontainer - -# FFI -/deps/libffi/ @nodejs/ffi -/doc/api/ffi.md @nodejs/ffi -/lib/ffi.js @nodejs/ffi -/src/ffi/ @nodejs/ffi -/src/node_ffi.* @nodejs/ffi -/test/ffi/ @nodejs/ffi diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index dc01fd3fdac5..000000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,14 +0,0 @@ -blank_issues_enabled: true -contact_links: - - name: ⁉️ Need general help with Node.js? - url: https://github.com/nodejs/help - about: Please file an issue in our help repo. - - name: 📦 Have an issue with npm? - url: https://github.com/npm/cli/issues - about: npm has a separate issue tracker. - - name: 📡 Have an issue with undici? (`WebSocket`, `fetch`, etc.) - url: https://github.com/nodejs/undici/issues - about: Undici has a separate issue tracker. - - name: 🌐 Found a problem with nodejs.org beyond the API reference docs? - url: https://github.com/nodejs/nodejs.org/issues/new/choose - about: Please file an issue in the Node.js website repo. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 936c2a061257..000000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,41 +0,0 @@ - diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md deleted file mode 100644 index 4613512bf2d0..000000000000 --- a/.github/SUPPORT.md +++ /dev/null @@ -1,27 +0,0 @@ -# Support - -Node.js contributors have limited availability to address general support -questions. Please make sure you are using a [currently-supported version of -Node.js](https://github.com/nodejs/Release#release-schedule). - -When looking for support, please first search for your question in these venues: - -* [Node.js Website](https://nodejs.org/en/), especially the - [API docs](https://nodejs.org/api/) or the [Learn material](https://nodejs.org/en/learn) -* [Node.js's GitHub Help repository](https://github.com/nodejs/help) -* [Open or closed issues in the Node.js GitHub organization](https://github.com/issues?utf8=%E2%9C%93&q=sort%3Aupdated-desc+org%3Anodejs+is%3Aissue) -* [Ask for support on Node.js's Discord server](https://nodejs.org/discord) - -If you didn't find an answer in the resources above, try these unofficial -resources: - -* [Questions tagged 'node.js' on Stack Overflow](https://stackoverflow.com/questions/tagged/node.js) -* [#node.js channel on libera.chat](https://web.libera.chat?channels=node.js&uio=d4) -* [Node.js Slack Community](https://node-js.slack.com/) - * To register: [nodeslackers.com](https://www.nodeslackers.com/) - -**GitHub issues are for tracking enhancements and bugs, not general support.** - -The open source license grants you the freedom to use Node.js. It does not -guarantee commitments of other people's time. Please be respectful and manage -your expectations. diff --git a/.github/actions/build-test-branch/action.yml b/.github/actions/build-test-branch/action.yml new file mode 100644 index 000000000000..76a5f7911976 --- /dev/null +++ b/.github/actions/build-test-branch/action.yml @@ -0,0 +1,6 @@ +name: "Build/Test" +description: "Trigger build/test run" + +runs: + using: "node20" + main: "./main.js" diff --git a/.github/actions/build-test-branch/main.js b/.github/actions/build-test-branch/main.js new file mode 100644 index 000000000000..0ace318ea981 --- /dev/null +++ b/.github/actions/build-test-branch/main.js @@ -0,0 +1,79 @@ + +const { + getLatestRevision, + sendBuildTestRequest, + newTask, +} = require("../utils"); + +const branchName = process.env.GITHUB_REF_NAME; +console.log("BranchName", branchName); + +const nodeRevision = getLatestRevision(); + +const driverRevision = process.env.INPUT_DRIVER_REVISION; +console.log("DriverRevision", driverRevision); + +const clobberInput = process.env.INPUT_CLOBBER; +console.log("Clobber", clobberInput); +const clobber = clobberInput == "true"; + +const slotInput = process.env.INPUT_SLOT; +console.log("Slot", slotInput); +const slot = slotInput ? +slotInput : undefined; + +let requestName = `Node Build/Test Branch ${branchName} ${nodeRevision}`; +if (driverRevision) { + requestName += ` driver ${driverRevision}`; +} +if (slot) { + requestName += ` slot ${slot}`; +} + +sendBuildTestRequest({ + name: requestName, + tasks: [ + ...platformTasks("macOS"), + ...platformTasks("linux"), + ], +}); + +function platformTasks(platform) { + const buildTask = newTask( + `Build Node ${platform}`, + { + kind: "BuildRuntime", + runtime: "node", + revision: nodeRevision, + branch: branchName, + branchSlot: slot, + driverRevision, + clobber, + }, + platform + ); + + const testTask = newTask( + `Test Node ${platform}`, + { + kind: "RunTestSuite", + runtime: "node", + revision: nodeRevision, + driverRevision, + }, + platform, + [buildTask] + ); + + const jestTask = newTask( + `Run random Jest tests ${platform}`, + { + kind: "JestTests", + revision: nodeRevision, + driverRevision, + }, + platform, + [buildTask] + ); + + return [buildTask, testTask, jestTask]; +} diff --git a/.github/actions/build-test/action.yml b/.github/actions/build-test/action.yml new file mode 100644 index 000000000000..76a5f7911976 --- /dev/null +++ b/.github/actions/build-test/action.yml @@ -0,0 +1,6 @@ +name: "Build/Test" +description: "Trigger build/test run" + +runs: + using: "node20" + main: "./main.js" diff --git a/.github/actions/build-test/main.js b/.github/actions/build-test/main.js new file mode 100644 index 000000000000..eace5b00c7c1 --- /dev/null +++ b/.github/actions/build-test/main.js @@ -0,0 +1,56 @@ + +const { + getLatestRevision, + sendBuildTestRequest, + newTask, +} = require("../utils"); + +const revision = getLatestRevision(); + +const clobberInput = process.env.INPUT_CLOBBER; +console.log("Clobber", clobberInput); +const clobber = clobberInput == "true"; + +sendBuildTestRequest({ + name: `Node Build/Test ${revision}`, + tasks: [ + ...platformTasks("macOS"), + ...platformTasks("linux"), + ], +}); + +function platformTasks(platform) { + const buildTask = newTask( + `Build Node ${platform}`, + { + kind: "BuildRuntime", + runtime: "node", + revision, + clobber, + }, + platform + ); + + const testTask = newTask( + `Test Node ${platform}`, + { + kind: "RunTestSuite", + runtime: "node", + revision, + }, + platform, + [buildTask] + ); + + const jestTask = newTask( + `Run random Jest tests ${platform}`, + { + kind: "JestTests", + revision, + }, + platform, + [buildTask] + ); + + return [buildTask, testTask, jestTask]; +} diff --git a/.github/actions/release/action.yml b/.github/actions/release/action.yml new file mode 100644 index 000000000000..d8e23a627422 --- /dev/null +++ b/.github/actions/release/action.yml @@ -0,0 +1,6 @@ +name: "Release" +description: "Trigger browser release" + +runs: + using: "node20" + main: "./main.js" diff --git a/.github/actions/release/main.js b/.github/actions/release/main.js new file mode 100644 index 000000000000..2353f83db454 --- /dev/null +++ b/.github/actions/release/main.js @@ -0,0 +1,30 @@ + +const { + getLatestRevision, + sendBuildTestRequest, + spawnChecked, + newTask, +} = require("../utils"); + +const revision = getLatestRevision(); + +sendBuildTestRequest({ + name: `Node Release ${revision}`, + tasks: [ + ...platformTasks("macOS"), + ...platformTasks("linux"), + ], +}); + +function platformTasks(platform) { + const releaseTask = newTask( + `Release Node ${platform}`, + { + kind: "ReleaseRuntime", + runtime: "node", + revision, + }, + platform + ); + return [releaseTask]; +} diff --git a/.github/actions/utils.js b/.github/actions/utils.js new file mode 100644 index 000000000000..f7ac64156b2e --- /dev/null +++ b/.github/actions/utils.js @@ -0,0 +1,83 @@ + +const http = require("http"); +const https = require("https"); +const { spawnSync } = require("child_process"); + +const node = `${__dirname}/../..`; + +function getLatestRevision() { + return spawnChecked("git", ["rev-parse", "--short=12", "HEAD"], { cwd: node }) + .stdout.toString() + .trim(); +} + +function sendBuildTestRequest(contents) { + const text = JSON.stringify(contents); + + const headers = { + "Content-Type": "application/json", + "Content-Length": text.length, + Authorization: process.env.BUILD_TEST_AUTHORIZATION, + }; + + // Allow overriding the build/test connection info for testing. + const options = { + hostname: process.env.BUILD_TEST_HOSTNAME || "build-test.replay.io", + port: process.env.BUILD_TEST_PORT || 443, + path: "/", + method: "POST", + headers, + }; + + const request = (process.env.BUILD_TEST_INSECURE ? http : https).request( + options, + response => { + console.log(`RequestFinished Code ${response.statusCode}`); + process.exit(response.statusCode == 200 ? 0 : 1); + } + ); + request.on("error", e => { + throw new Error(`Error contacting build/test server: ${e}`); + }); + request.write(text); + request.end(); + + setTimeout(() => { + console.log("Timed out waiting for build/test server response"); + process.exit(1); + }, 30000); +} + +function spawnChecked(cmd, args, options) { + const prettyCmd = [cmd].concat(args).join(" "); + console.error(prettyCmd); + + const rv = spawnSync(cmd, args, options); + + if (rv.status != 0 || rv.error) { + console.error(rv.error); + throw new Error(`Spawned process failed with exit code ${rv.status}`); + } + + return rv; +} + +let gNextTaskId = 1; + +function newTask(name, task, platform, dependencies = []) { + const id = gNextTaskId++; + return { + id, + name, + task, + platform, + dependencies: dependencies.map(task => task.id), + }; +} + +module.exports = { + getLatestRevision, + sendBuildTestRequest, + spawnChecked, + newTask, +}; diff --git a/.github/approveman.yml b/.github/approveman.yml new file mode 100644 index 000000000000..76f180f9df65 --- /dev/null +++ b/.github/approveman.yml @@ -0,0 +1,5 @@ +--- +ownership_rules: + directory_matching_rules: + - name: code for testing backend + path: replay-test/** diff --git a/.github/replay-soc2.yml b/.github/replay-soc2.yml new file mode 100644 index 000000000000..4b5813c59a06 --- /dev/null +++ b/.github/replay-soc2.yml @@ -0,0 +1,16 @@ +--- +# This is the block list used for the Replay SOC2 bot. +# +# An example: +# blocklist: +# - bad/** +# - rootfile +# +# Each line maps to a glob expression that gets applied to the list of files +# that the PR touches. The Replay SOC2 bot will never approve any changes to +# .github/replay_soc2.yml. The matching is done by minimatch. +# +allowlist: + - replay-test/** + - .github/** +blocklist: diff --git a/.github/workflows/auto-start-ci.yml b/.github/workflows/auto-start-ci.yml deleted file mode 100644 index 34488eeed6d7..000000000000 --- a/.github/workflows/auto-start-ci.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Auto Start CI - -on: - schedule: - # Runs every five minutes (fastest the scheduler can run). Five minutes is - # optimistic, it can take longer to run. - # To understand why `schedule` is used instead of other events, refer to - # ./doc/contributing/commit-queue.md - - cron: '*/5 * * * *' - -concurrency: ${{ github.workflow }} - -env: - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - get-prs-for-ci: - permissions: - pull-requests: read - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - outputs: - numbers: ${{ steps.get_prs_for_ci.outputs.numbers }} - steps: - - name: Get Pull Requests - id: get_prs_for_ci - run: > - echo "numbers=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --label 'request-ci' \ - --json 'number' \ - --search 'review:approved' \ - -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 5)" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - start-ci: - permissions: - contents: read - pull-requests: write - needs: get-prs-for-ci - if: needs.get-prs-for-ci.outputs.numbers != '' - runs-on: ubuntu-slim - steps: - - name: Install Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Install @node-core/utils - run: npm install -g @node-core/utils - - - name: Setup @node-core/utils - run: | - ncu-config set username "$USERNAME" - ncu-config set token "$GH_TOKEN" - ncu-config set jenkins_token "$JENKINS_TOKEN" - ncu-config set owner "$GITHUB_REPOSITORY_OWNER" - ncu-config set repo "$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)" - env: - USERNAME: ${{ secrets.JENKINS_USER }} - GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} - JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} - - - name: Start the CI - run: | - curl -fsSL "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/tools/actions/start-ci.sh" \ - | sh -s -- ${{ needs.get-prs-for-ci.outputs.numbers }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_REPO: ${{ github.repository }} diff --git a/.github/workflows/build-tarball.yml b/.github/workflows/build-tarball.yml deleted file mode 100644 index c9cf288dc938..000000000000 --- a/.github/workflows/build-tarball.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Build from tarball - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths-ignore: - - '**.md' - - '**.nix' - - eslint.config.mjs - - '**/eslint.config_partial.mjs' - - android-configure - - android-configure.py - - android-patches/** - - benchmarks/** - - codecov.yml - - doc/** - - pyproject.yml - - tsconfig.json - - test/internet/** - - tools/actions/** - - tools/dep_updaters/** - - tools/doc/** - - tools/eslint-rules/** - - tools/eslint/** - - tools/lint-md/** - - typings/** - - vcbuild.bat - - .** - - '!.github/workflows/build-tarball.yml' - push: - branches: - - main - - v[0-9]+.x-staging - - v[0-9]+.x - paths-ignore: - - '**.md' - - '**.nix' - - eslint.config.mjs - - '**/eslint.config_partial.mjs' - - android-configure - - android-configure.py - - android-patches/** - - benchmarks/** - - codecov.yml - - doc/** - - pyproject.yml - - tsconfig.json - - test/internet/** - - tools/actions/** - - tools/dep_updaters/** - - tools/doc/** - - tools/eslint-rules/** - - tools/eslint/** - - tools/lint-md/** - - typings/** - - vcbuild.bat - - .** - - '!.github/workflows/build-tarball.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - -permissions: - contents: read - -jobs: - build-tarball: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Environment Information - run: npx envinfo - - name: Make tarball - run: | - export DISTTYPE=nightly - export DATESTRING=$(date "+%Y-%m-%d") - export COMMIT=$(git rev-parse --short=10 "$GITHUB_SHA") - ./configure && make tar -j4 SKIP_XZ=1 - - name: Upload tarball artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: tarballs - path: '*.tar.gz' - compression-level: 0 - test-tarball-linux: - needs: build-tarball - runs-on: ubuntu-24.04-arm - env: - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 - CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang++-19 - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: .github/actions/install-clang - sparse-checkout-cone-mode: false - - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang - with: - clang-version: ${{ env.CLANG_VERSION }} - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Set up sccache - if: github.base_ref == 'main' || github.ref_name == 'main' - uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: v0.12.0 - - name: Environment Information - run: npx envinfo - - name: Download tarball - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: tarballs - path: tarballs - - name: Extract tarball - run: | - tar xzf tarballs/*.tar.gz -C "$RUNNER_TEMP" - echo "TAR_DIR=$RUNNER_TEMP/$(basename tarballs/*.tar.gz .tar.gz)" >> "$GITHUB_ENV" - - name: Build - run: make -C "$TAR_DIR" build-ci -j4 V=1 - - name: Test - run: make -C "$TAR_DIR" test-ci -j1 V=1 TEST_CI_ARGS="-p dots --measure-flakiness 9" diff --git a/.github/workflows/build-test-branch.yml b/.github/workflows/build-test-branch.yml new file mode 100644 index 000000000000..cdb55f359451 --- /dev/null +++ b/.github/workflows/build-test-branch.yml @@ -0,0 +1,36 @@ +name: Build/Test Branch +on: + workflow_dispatch: + inputs: + driver_revision: + description: "Driver revision to use, if not the latest" + required: false + clobber: + description: "Build node from scratch" + type: boolean + required: false + slot: + description: "Checked out repository slot to use" + required: false + +jobs: + build-test: + name: Trigger build/test run on current branch + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Connect to Tailscale + uses: tailscale/github-action@v3 + with: + authkey: ${{ secrets.TAILSCALE_API_KEY }} + tags: tag:node-fork-ci + - uses: actions/checkout@v2 + - uses: ./.github/actions/build-test-branch + env: + BUILD_TEST_AUTHORIZATION: ${{ secrets.BUILD_TEST_AUTHORIZATION }} + BUILD_TEST_HOSTNAME: a49855c191a944333918aea7ad31bc76-6a8a830d89921d8a.elb.us-east-2.amazonaws.com + BUILD_TEST_PORT: ${{ secrets.BUILD_TEST_PORT }} + BUILD_TEST_INSECURE: ${{ secrets.BUILD_TEST_INSECURE }} + INPUT_DRIVER_REVISION: ${{ github.event.inputs.driver_revision }} + INPUT_CLOBBER: ${{ github.event.inputs.clobber }} + INPUT_SLOT: ${{ github.event.inputs.slot }} diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 000000000000..aa7dc83f9f78 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,32 @@ +name: Build/Test +on: + workflow_dispatch: + inputs: + clobber: + description: "Build node from scratch" + type: boolean + required: false + pull_request: + push: + branches: + - master + +jobs: + build-test: + name: Trigger build/test run + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Connect to Tailscale + uses: tailscale/github-action@v3 + with: + authkey: ${{ secrets.TAILSCALE_API_KEY }} + tags: tag:node-fork-ci + - uses: actions/checkout@v2 + - uses: ./.github/actions/build-test + env: + BUILD_TEST_AUTHORIZATION: ${{ secrets.BUILD_TEST_AUTHORIZATION }} + BUILD_TEST_HOSTNAME: a49855c191a944333918aea7ad31bc76-6a8a830d89921d8a.elb.us-east-2.amazonaws.com + BUILD_TEST_PORT: ${{ secrets.BUILD_TEST_PORT }} + BUILD_TEST_INSECURE: ${{ secrets.BUILD_TEST_INSECURE }} + INPUT_CLOBBER: ${{ github.event.inputs.clobber }} diff --git a/.github/workflows/close-stalled.yml b/.github/workflows/close-stalled.yml deleted file mode 100644 index 56c0f2542b9e..000000000000 --- a/.github/workflows/close-stalled.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Close stalled issues and PRs -on: - schedule: - - cron: 0 0 * * * - -env: - CLOSE_MESSAGE: > - Closing this because it has stalled. Feel free to reopen if this issue/PR - is still relevant, or to ping the collaborator who labelled it stalled if - you have any questions. - -permissions: - contents: read - -jobs: - stale: - permissions: - issues: write # for actions/stale to close stale issues - pull-requests: write # for actions/stale to close stale PRs - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - steps: - - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-close: 30 - stale-pr-label: stalled - stale-issue-label: stalled - close-issue-message: ${{ env.CLOSE_MESSAGE }} - close-pr-message: ${{ env.CLOSE_MESSAGE }} - # used to filter issues to check whether or not should be closed, avoids hitting maximum operations allowed if needing to paginate through all open issues - only-labels: stalled - # max requests it will send per run to the GitHub API before it deliberately exits to avoid hitting API rate limits - operations-per-run: 500 - # deactivates automatic stale labelling as we prefer to do that manually - days-before-stale: -1 diff --git a/.github/workflows/comment-labeled.yml b/.github/workflows/comment-labeled.yml deleted file mode 100644 index e66525074841..000000000000 --- a/.github/workflows/comment-labeled.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Comment on issues and PRs when labeled -on: - issues: - types: [labeled] - pull_request_target: - types: [labeled] - -env: - STALE_MESSAGE: > - This issue/PR was marked as stalled, it will be automatically closed in 30 days. - If it should remain open, please leave a comment explaining why it should remain open. - FAST_TRACK_MESSAGE: Fast-track has been requested by @${{ github.actor }}. Please 👍 to approve. - NOTABLE_CHANGE_MESSAGE: | - The https://github.com/nodejs/node/labels/notable-change label has been added by @${{ github.actor }}. - - Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. Otherwise, the commit will be placed in the _Other Notable Changes_ section. - -permissions: - contents: read - -jobs: - stale-comment: - permissions: - issues: write - pull-requests: write - if: github.repository == 'nodejs/node' && github.event.label.name == 'stalled' - runs-on: ubuntu-slim - steps: - - name: Post stalled comment - env: - NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh issue comment "$NUMBER" --repo ${{ github.repository }} --body "$STALE_MESSAGE" - - fast-track: - permissions: - pull-requests: write - if: github.repository == 'nodejs/node' && github.event_name == 'pull_request_target' && github.event.label.name == 'fast-track' - runs-on: ubuntu-slim - steps: - - name: Request Fast-Track - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "$FAST_TRACK_MESSAGE" - - notable-change: - permissions: - pull-requests: write - if: github.repository == 'nodejs/node' && github.event_name == 'pull_request_target' && github.event.label.name == 'notable-change' - runs-on: ubuntu-slim - steps: - - name: Add notable change description - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "$NOTABLE_CHANGE_MESSAGE" diff --git a/.github/workflows/commit-lint-problem-matcher.json b/.github/workflows/commit-lint-problem-matcher.json deleted file mode 100644 index 72dd13b9e092..000000000000 --- a/.github/workflows/commit-lint-problem-matcher.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "core-validate-commit", - "pattern": [ - { - "regexp": "^not ok \\d+ (.*)$", - "message": 1 - } - ] - } - ] -} diff --git a/.github/workflows/commit-lint.yml b/.github/workflows/commit-lint.yml deleted file mode 100644 index 93ca3425be49..000000000000 --- a/.github/workflows/commit-lint.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: First commit message adheres to guidelines - -on: - pull_request: - branches: - - main - -env: - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - lint-commit-message: - runs-on: ubuntu-slim - steps: - - name: Compute number of commits in the PR - id: nb-of-commits - run: | - echo "plusOne=$((${{ github.event.pull_request.commits }} + 1))" >> $GITHUB_OUTPUT - echo "minusOne=$((${{ github.event.pull_request.commits }} - 1))" >> $GITHUB_OUTPUT - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: ${{ steps.nb-of-commits.outputs.plusOne }} - persist-credentials: false - - run: git reset HEAD^2 - - name: Install Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Validate commit message - run: | - echo "::add-matcher::.github/workflows/commit-lint-problem-matcher.json" - git rev-parse HEAD~${{ steps.nb-of-commits.outputs.minusOne }} | xargs npx -q core-validate-commit --no-validate-metadata --tap diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml deleted file mode 100644 index 8b5f37807b12..000000000000 --- a/.github/workflows/commit-queue.yml +++ /dev/null @@ -1,99 +0,0 @@ -# This action requires the following secrets to be set on the repository: -# GH_USER_TOKEN: GitHub user token, to be used by ncu and to push changes -# JENKINS_USER: GitHub user whose Jenkins token is defined below -# JENKINS_TOKEN: Jenkins token, to be used to check CI status - -name: Commit Queue - -on: - # `schedule` event is used instead of `pull_request` because when a - # `pull_request` event is triggered on a PR from a fork, GITHUB_TOKEN will - # be read-only, and the Action won't have access to any other repository - # secrets, which it needs to access Jenkins API. - schedule: - - cron: '*/5 * * * *' - -concurrency: ${{ github.workflow }} - -env: - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - get_mergeable_prs: - permissions: - pull-requests: read - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - outputs: - numbers: ${{ steps.get_mergeable_prs.outputs.numbers }} - steps: - - name: Get Pull Requests - id: get_mergeable_prs - run: | - prs=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --base "$GITHUB_REF_NAME" \ - --label 'commit-queue' \ - --json 'number' \ - --search "created:<=$(date --date="2 days ago" +"%Y-%m-%dT%H:%M:%S%z") -label:blocked" \ - -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 100) - fast_track_prs=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --base "$GITHUB_REF_NAME" \ - --label 'commit-queue' \ - --label 'fast-track' \ - --search "-label:blocked" \ - --json 'number' \ - -t '{{ range . }}{{ .number }} {{ end }}' \ - --limit 100) - numbers=$(echo $prs' '$fast_track_prs | jq -r -s 'unique | join(" ")') - echo "numbers=$numbers" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - commitQueue: - needs: get_mergeable_prs - if: needs.get_mergeable_prs.outputs.numbers != '' - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - # A personal token is required because pushing with GITHUB_TOKEN will - # prevent commits from running CI after they land. It needs - # to be set here because `checkout` configures GitHub authentication - # for push as well. - token: ${{ secrets.GH_USER_TOKEN }} - - # Install dependencies - - name: Install Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Install @node-core/utils - run: npm install -g @node-core/utils - - - name: Set variables - run: | - echo "REPOSITORY=$(echo "$GITHUB_REPOSITORY" | cut -d/ -f2)" >> "$GITHUB_ENV" - - - name: Configure @node-core/utils - run: | - ncu-config set branch "${GITHUB_REF_NAME}" - ncu-config set upstream origin - ncu-config set username "$USERNAME" - ncu-config set token "$GITHUB_TOKEN" - ncu-config set jenkins_token "$JENKINS_TOKEN" - ncu-config set repo "${REPOSITORY}" - ncu-config set owner "${GITHUB_REPOSITORY_OWNER}" - env: - USERNAME: ${{ secrets.JENKINS_USER }} - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - JENKINS_TOKEN: ${{ secrets.JENKINS_TOKEN }} - - - name: Start the Commit Queue - run: ./tools/actions/commit-queue.sh "${GITHUB_REPOSITORY_OWNER}" "${REPOSITORY}" ${{ needs.get_mergeable_prs.outputs.numbers }} - env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} diff --git a/.github/workflows/coverage-linux.yml b/.github/workflows/coverage-linux.yml deleted file mode 100644 index 4a60b092dbc9..000000000000 --- a/.github/workflows/coverage-linux.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Coverage Linux - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - lib/**/*.js - - Makefile - - src/**/*.cc - - src/**/*.h - - test/** - - tools/gyp/** - - tools/test.py - - .github/workflows/coverage-linux.yml - - codecov.yml - - .nycrc - push: - branches: - - main - paths: - - lib/**/*.js - - Makefile - - src/**/*.cc - - src/**/*.h - - test/** - - tools/gyp/** - - tools/test.py - - .github/workflows/coverage-linux.yml - - codecov.yml - - .nycrc - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 - CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang++-19 - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - -permissions: - contents: read - -jobs: - coverage-linux: - if: github.event.pull_request.draft == false - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang - with: - clang-version: ${{ env.CLANG_VERSION }} - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Set up sccache - if: github.base_ref == 'main' || github.ref_name == 'main' - uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: v0.12.0 - - name: Environment Information - run: npx envinfo - - name: Install gcovr - run: pip install gcovr==7.2 - - name: Configure - run: ./configure --verbose --error-on-warn --coverage - # TODO(bcoe): fix the couple tests that fail with the inspector enabled. - # The cause is most likely coverage's use of the inspector. - - name: Build and test - run: NODE_V8_COVERAGE=coverage/tmp make test-cov -j4 V=1 TEST_CI_ARGS="-p dots --measure-flakiness 9" || exit 0 - - name: Report JS - run: npx c8 report --check-coverage - env: - NODE_OPTIONS: --max-old-space-size=8192 - - name: Report C++ - run: gcovr --object-directory=out -v --filter src --xml -o ./coverage/coverage-cxx.xml --root=./ --gcov-executable="llvm-cov-18 gcov" - # Clean temporary output from gcov and c8, so that it's not uploaded: - - name: Clean tmp - run: rm -rf coverage/tmp && rm -rf out - - name: Upload - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 - with: - directory: ./coverage diff --git a/.github/workflows/coverage-windows.yml b/.github/workflows/coverage-windows.yml deleted file mode 100644 index 607e27a8d6cd..000000000000 --- a/.github/workflows/coverage-windows.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Coverage Windows - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths-ignore: - - '**.md' - - '**.nix' - - eslint.config.mjs - - '**/eslint.config_partial.mjs' - - android-configure - - android-configure.py - - android-patches/** - - benchmarks/** - - doc/** - - pyproject.yml - - tsconfig.json - - test/internet/** - - tools/actions/** - - tools/bootstrap/** - - tools/dep_updaters/** - - tools/doc/** - - tools/eslint-rules/** - - tools/eslint/** - - tools/lint-md/** - - typings/** - - .** - - '!.github/workflows/coverage-windows.yml' - push: - branches: - - main - paths-ignore: - - '**.md' - - '**.nix' - - eslint.config.mjs - - '**/eslint.config_partial.mjs' - - android-configure - - android-configure.py - - android-patches/** - - benchmarks/** - - doc/** - - pyproject.yml - - tsconfig.json - - test/internet/** - - tools/actions/** - - tools/bootstrap/** - - tools/dep_updaters/** - - tools/doc/** - - tools/eslint-rules/** - - tools/eslint/** - - tools/lint-md/** - - typings/** - - .** - - '!.github/workflows/coverage-windows.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - -permissions: - contents: read - -jobs: - coverage-windows: - if: github.event.pull_request.draft == false - runs-on: windows-2025 - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Install deps - run: choco install nasm - - name: Install Rust ${{ env.RUSTC_VERSION }} - run: | - rustup override set "$RUSTC_VERSION" - rustup --version - - name: Environment Information - run: npx envinfo - - name: Build - run: ./vcbuild.bat clang-cl v8temporal - # TODO(bcoe): investigate tests that fail with coverage enabled - # on Windows. - - name: Test - run: ./vcbuild.bat noprojgen nobuild test-ci-js; node -e 'process.exit(0)' - env: - NODE_V8_COVERAGE: ./coverage/tmp - - name: Report - run: npx c8 report - env: - NODE_OPTIONS: --max-old-space-size=8192 - - name: Clean tmp - run: npx rimraf ./coverage/tmp - - name: Upload - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 - with: - directory: ./coverage diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml deleted file mode 100644 index 23ffe66fb244..000000000000 --- a/.github/workflows/daily.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Node.js daily job - -on: - workflow_dispatch: - schedule: - - cron: 0 0 * * * - -env: - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - build-lto: - if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - - name: Build lto - run: | - sudo apt-get update && sudo apt-get install ninja-build -y - ./configure --enable-lto --ninja - ninja -C out/Release diff --git a/.github/workflows/find-inactive-collaborators.yml b/.github/workflows/find-inactive-collaborators.yml deleted file mode 100644 index 35a0f4b1f5c7..000000000000 --- a/.github/workflows/find-inactive-collaborators.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Find inactive collaborators - -on: - schedule: - # Run every Monday at 4:05 AM UTC. - - cron: 5 4 * * 1 - - workflow_dispatch: - -env: - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - find: - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Find inactive collaborators - run: tools/find-inactive-collaborators.mjs - - - name: Open pull request - uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 - # Creates a PR or update the Action's existing PR, or - # no-op if the base branch is already up-to-date. - env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - with: - author: Node.js GitHub Bot - branch: actions/inactive-collaborators - body: | - This PR was generated by the [`find-inactive-collaborators.yml` workflow](https://github.com/nodejs/node/blob/main/.github/workflows/find-inactive-collaborators.yml). - - @nodejs/tsc Please follow up with the [offboarding tasks](https://github.com/nodejs/node/blob/main/doc/contributing/offboarding.md). - commit-message: 'meta: move one or more collaborators to emeritus' - labels: meta - title: 'meta: move one or more collaborators to emeritus' diff --git a/.github/workflows/find-inactive-tsc.yml b/.github/workflows/find-inactive-tsc.yml deleted file mode 100644 index 8deeaccf5dfd..000000000000 --- a/.github/workflows/find-inactive-tsc.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Find inactive TSC voting members - -on: - schedule: - # Run every Tuesday 12:05 AM UTC. - - cron: 5 0 * * 2 - - workflow_dispatch: - -env: - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - find: - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - - steps: - - name: Checkout the repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Clone nodejs/TSC repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - path: .tmp - persist-credentials: false - repository: nodejs/TSC - - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - - name: Find inactive TSC voting members - run: tools/find-inactive-tsc.mjs >> $GITHUB_ENV - - - name: Open pull request - uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 - # Creates a PR or update the Action's existing PR, or - # no-op if the base branch is already up-to-date. - env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - with: - author: Node.js GitHub Bot - branch: actions/inactive-tsc - body: | - This PR was generated by tools/find-inactive-tsc.yml. - - @nodejs/tsc ${{ env.INACTIVE_TSC_HANDLES }} - - ${{ env.DETAILS_FOR_COMMIT_BODY }} - commit-message: 'meta: move TSC voting member(s) to regular member(s)' - labels: meta - title: 'meta: move TSC voting member(s) to regular member(s)' - update-pull-request-title-and-body: true diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml deleted file mode 100644 index 03aa1fa9a8d0..000000000000 --- a/.github/workflows/label-pr.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Label PRs - -on: - pull_request_target: - types: [opened] - -permissions: - contents: read - -jobs: - label: - runs-on: ubuntu-slim - - steps: - - uses: nodejs/node-pr-labeler@d4cf1b8b9f23189c37917000e5e17e796c770a6b # v1 - with: - repo-token: ${{ secrets.GH_USER_TOKEN }} - configuration-path: .github/label-pr-config.yml diff --git a/.github/workflows/license-builder.yml b/.github/workflows/license-builder.yml deleted file mode 100644 index 36d303f9e121..000000000000 --- a/.github/workflows/license-builder.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: License update -on: - schedule: - # 00:00:00 every Monday - # https://crontab.guru/#0_0_*_*_1 - - cron: 0 0 * * 1 - workflow_dispatch: - -permissions: - contents: read - -jobs: - update_license: - permissions: - contents: write # for gr2m/create-or-update-pull-request-action to push local changes - pull-requests: write # for gr2m/create-or-update-pull-request-action to create a PR - if: github.repository == 'nodejs/node' - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - run: ./tools/license-builder.sh # Run the license builder tool - - uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 - # Creates a PR or update the Action's existing PR, or - # no-op if the base branch is already up-to-date. - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - author: Node.js GitHub Bot - branch: actions/license-builder - title: 'doc: run license-builder' - body: > - License is likely out of date. This is an automatically generated PR by - the `license-builder.yml` GitHub Action, which runs `license-builder.sh` - and submits a new PR or updates an existing PR. - commit-message: 'doc: run license-builder' - labels: meta diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 index eb107edccdd9..000000000000 --- a/.github/workflows/linters.yml +++ /dev/null @@ -1,285 +0,0 @@ -name: Linters - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - push: - branches: - - main - - v[0-9]+.x-staging - - v[0-9]+.x - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - NODE_VERSION: lts/* - -permissions: - contents: read - -jobs: - lint-addon-docs: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - - name: Lint addon docs - run: NODE=$(command -v node) make lint-addon-docs - lint-cpp: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Environment Information - run: npx envinfo - - name: Lint C/C++ files - run: make lint-cpp - format-cpp: - if: ${{ github.event.pull_request && github.event.pull_request.draft == false && github.base_ref == github.event.repository.default_branch }} - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 0 - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Environment Information - run: npx envinfo - - name: Format C/C++ files - run: | - make format-cpp-build - # The `make format-cpp` error code is intentionally ignored here - # because it is irrelevant. We already check if the formatter produced - # a diff in the next line. - # Refs: https://github.com/nodejs/node/pull/42764 - CLANG_FORMAT_START="$(git merge-base HEAD refs/remotes/origin/$GITHUB_BASE_REF)" \ - make format-cpp || true - git --no-pager diff --exit-code && EXIT_CODE="$?" || EXIT_CODE="$?" - if [ "$EXIT_CODE" != "0" ] - then - echo - echo 'ERROR: Please run:' - echo - echo " CLANG_FORMAT_START="$\(git merge-base HEAD ${GITHUB_BASE_REF}\)" make format-cpp" - echo - echo 'to format the commits in your branch.' - exit "$EXIT_CODE" - fi - lint-js-and-md: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Environment Information - run: npx envinfo - - name: Lint JavaScript files - run: | - set +e - NODE=$(command -v node) make lint-js - EXIT_CODE="$?" - if [ "$EXIT_CODE" != "0" ]; then - echo - echo 'ERROR: The JavaScript lint validation failed (the errors are logged above).' - echo ' Please fix the lint errors.' - if NODE=$(command -v node) make lint-js-fix > /dev/null 2>&1; then - echo ' Run:' - echo ' make lint-js-fix' - echo ' to fix the lint issues.' - git --no-pager diff - elif git diff --quiet --exit-code; then - echo ' None of the issue is auto-fixable, so manual fixes for' - echo ' all of the issues are required.' - else - echo ' Run:' - echo ' make lint-js-fix' - echo ' to fix the auto-fixable lint issues.' - echo ' Note that some manual fixes are also required.' - fi - echo - exit "$EXIT_CODE" - fi - - name: Get release version numbers - if: ${{ github.event.pull_request && github.event.pull_request.base.ref == github.event.pull_request.base.repo.default_branch }} - id: get-released-versions - run: ./tools/lint-md/list-released-versions-from-changelogs.mjs >> $GITHUB_OUTPUT - - name: Lint markdown files - run: | - echo "::add-matcher::.github/workflows/remark-lint-problem-matcher.json" - NODE=$(command -v node) make lint-md - env: - NODE_RELEASED_VERSIONS: ${{ steps.get-released-versions.outputs.NODE_RELEASED_VERSIONS }} - lint-nix: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: '*.nix' - sparse-checkout-cone-mode: false - - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - - name: Lint Nix files - run: | - nix-shell -I nixpkgs=./tools/nix/pkgs.nix -p 'nixfmt-tree' --run ' - treefmt --quiet --ci - ' && EXIT_CODE="$?" || EXIT_CODE="$?" - if [ "$EXIT_CODE" != "0" ] - then - git --no-pager diff || true - exit "$EXIT_CODE" - fi - - lint-py: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: | - /Makefile - /benchmark/ - /doc/ - /lib/ - /src/node_version.h - /tools/ - pyproject.toml - *.py - sparse-checkout-cone-mode: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Environment Information - run: npx envinfo - - name: Lint Python - run: | - make lint-py-build - make lint-py - lint-yaml: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: | - /Makefile - /tools/pip/ - *.yml - *.yaml - sparse-checkout-cone-mode: false - - name: Use Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Environment Information - run: npx envinfo - - name: Lint YAML - run: | - make lint-yaml-build || true - make lint-yaml - - lint-sh: - if: github.event.pull_request.draft == false - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: | - /tools/lint-sh.mjs - *.sh - sparse-checkout-cone-mode: false - - run: shellcheck -V - - name: Lint Shell scripts - run: tools/lint-sh.mjs . - lint-codeowners: - if: github.event.pull_request.draft == false - # cannot use ubuntu-slim here because mszostok/codeowners-validator is dockerized - # cannot use ubuntu-24.04-arm here because the docker image is x86 only - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - uses: mszostok/codeowners-validator@7f3f5e28c6d7b8dfae5731e54ce2272ca384592f - with: - checks: files,duppatterns - lint-pr-url: - if: ${{ github.event.pull_request }} - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - fetch-depth: 2 - persist-credentials: false - sparse-checkout: | - /tools/lint-pr-url.mjs - /doc/api/ - sparse-checkout-cone-mode: false - # GH Actions squashes all PR commits, HEAD^ refers to the base branch. - - run: git diff HEAD^ HEAD -G"pr-url:" -- "*.md" | ./tools/lint-pr-url.mjs ${{ github.event.pull_request.html_url }} - lint-readme: - runs-on: ubuntu-slim - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - sparse-checkout: | - README.md - /tools/lint-readme-lists.mjs - sparse-checkout-cone-mode: false - - name: Get team members if possible - if: ${{ (github.event.pull_request && github.event.pull_request.base.ref == github.event.pull_request.base.repo.default_branch) || github.event.ref == github.event.repository.default_branch }} - id: team_members - run: | - get_list_members() { - TEAM="$1" - QUOTE='"' - gh api "/orgs/nodejs/teams/$TEAM/members" -X GET -f per_page=100 --jq "map(.login) | ${QUOTE}${TEAM}=\(tojson)${QUOTE}" - } - [ -z "$GITHUB_TOKEN" ] || ( - get_list_members "collaborators" - get_list_members "issue-triage" - get_list_members "tsc" - ) >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - - run: tools/lint-readme-lists.mjs "$TEAMS" - env: - TEAMS: ${{ tojson(steps.team_members.outputs) }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..0e21ee2c4df6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Release Latest Browsers +on: + workflow_dispatch: + +jobs: + release: + name: Trigger release run + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Connect to Tailscale + uses: tailscale/github-action@v3 + with: + authkey: ${{ secrets.TAILSCALE_API_KEY }} + tags: tag:node-fork-ci + - uses: actions/checkout@v2 + - uses: ./.github/actions/release + env: + BUILD_TEST_AUTHORIZATION: ${{ secrets.BUILD_TEST_AUTHORIZATION }} + BUILD_TEST_HOSTNAME: a49855c191a944333918aea7ad31bc76-6a8a830d89921d8a.elb.us-east-2.amazonaws.com + BUILD_TEST_PORT: ${{ secrets.BUILD_TEST_PORT }} + BUILD_TEST_INSECURE: ${{ secrets.BUILD_TEST_INSECURE }} diff --git a/.github/workflows/remark-lint-problem-matcher.json b/.github/workflows/remark-lint-problem-matcher.json deleted file mode 100644 index cfb281310a9a..000000000000 --- a/.github/workflows/remark-lint-problem-matcher.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "problemMatcher": [ - { - "owner": "remark-lint", - "pattern": [ - { - "regexp": "^(?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)*$", - "file": 1 - }, - { - "regexp": "^\\s+(?:\\d+:\\d+-)?(\\d+):(\\d+)\\s+\\S*(error|warning|info)\\S*\\s+(.+)\\s+(\\S+)\\s+(?:\\S+)$", - "line": 1, - "column": 2, - "severity": 3, - "message": 4, - "code": 5, - "loop": true - } - ] - } - ] -} diff --git a/.github/workflows/test-internet.yml b/.github/workflows/test-internet.yml deleted file mode 100644 index cb8759c559ae..000000000000 --- a/.github/workflows/test-internet.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Test internet - -on: - workflow_dispatch: - schedule: - - cron: 5 0 * * * - - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths: - - .github/workflows/test-internet.yml - - test/internet/** - - internal/dns/** - - lib/dns.js - - lib/net.js - push: - branches: - - main - - canary - - v[0-9]+.x-staging - - v[0-9]+.x - paths: - - .github/workflows/test-internet.yml - - test/internet/** - - internal/dns/** - - lib/dns.js - - lib/net.js - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 - CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang++-19 - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - -permissions: - contents: read - -jobs: - test-internet: - if: (github.event_name == 'schedule' && github.repository == 'nodejs/node') || (github.event.pull_request && github.event.pull_request.draft == false) - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./.github/actions/install-clang - with: - clang-version: ${{ env.CLANG_VERSION }} - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Set up sccache - if: github.base_ref == 'main' || github.ref_name == 'main' - uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: v0.12.0 - - name: Environment Information - run: npx envinfo - - name: Build - run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn" - - name: Test Internet - run: make test-internet -j4 V=1; diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml deleted file mode 100644 index 4dbc51909191..000000000000 --- a/.github/workflows/test-linux.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Test Linux - -on: - pull_request: - paths-ignore: - - .mailmap - - README.md - - vcbuild.bat - - tools/actions/** - - tools/clang-format/** - - tools/dep_updaters/** - - test/internet/** - - '**.nix' - - .github/** - - '!.github/workflows/test-linux.yml' - types: [opened, synchronize, reopened, ready_for_review] - push: - branches: - - main - - canary - - v[0-9]+.x-staging - - v[0-9]+.x - paths-ignore: - - .mailmap - - README.md - - vcbuild.bat - - tools/actions/** - - tools/clang-format/** - - tools/dep_updaters/** - - test/internet/** - - '**.nix' - - .github/** - - '!.github/workflows/test-linux.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - FLAKY_TESTS: keep_retrying - CLANG_VERSION: '19' - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang-19 - CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} clang++-19 - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - RUSTC_VERSION: '1.86' - -permissions: - contents: read - -jobs: - test-linux: - if: github.event.pull_request.draft == false - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-24.04, ubuntu-24.04-arm] - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - path: node - - name: Install Clang ${{ env.CLANG_VERSION }} - uses: ./node/.github/actions/install-clang - with: - clang-version: ${{ env.CLANG_VERSION }} - - name: Install Rust ${{ env.RUSTC_VERSION }} - run: | - rustup override set "$RUSTC_VERSION" - rustup --version - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Set up sccache - if: github.base_ref == 'main' || github.ref_name == 'main' - uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: v0.12.0 - - name: Environment Information - run: npx envinfo - - name: Build - working-directory: node - run: make build-ci -j4 V=1 CONFIG_FLAGS="--error-on-warn --v8-enable-temporal-support" - - name: Test - working-directory: node - run: make test-ci -j1 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9" - - name: Ensure running tests did not cause any change in the tree - working-directory: node - run: git add -A && git diff --name-only --exit-code --staged - - name: Re-run test in a folder whose name contains unusual chars - run: | - mv node "$DIR" - cd "$DIR" - ./tools/test.py --flaky-tests keep_retrying -p actions -j 4 - env: - DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…` diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml deleted file mode 100644 index cf22fb2e6a5d..000000000000 --- a/.github/workflows/test-macos.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: Test macOS - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - paths-ignore: - - '**.md' - - '**.nix' - - eslint.config.mjs - - '**/eslint.config_partial.mjs' - - android-configure - - android-configure.py - - android-patches/** - - benchmarks/** - - codecov.yml - - doc/** - - pyproject.yml - - tsconfig.json - - test/internet/** - - tools/actions/** - - tools/dep_updaters/** - - tools/doc/** - - tools/eslint-rules/** - - tools/eslint/** - - tools/lint-md/** - - typings/** - - vcbuild.bat - - .** - - '!.github/workflows/test-macos.yml' - push: - branches: - - main - - canary - - v[0-9]+.x-staging - - v[0-9]+.x - paths-ignore: - - '**.md' - - '**.nix' - - eslint.config.mjs - - '**/eslint.config_partial.mjs' - - android-configure - - android-configure.py - - android-patches/** - - benchmarks/** - - codecov.yml - - doc/** - - pyproject.yml - - tsconfig.json - - test/internet/** - - tools/actions/** - - tools/dep_updaters/** - - tools/doc/** - - tools/eslint-rules/** - - tools/eslint/** - - tools/lint-md/** - - typings/** - - vcbuild.bat - - .** - - '!.github/workflows/test-macos.yml' - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PYTHON_VERSION: '3.14' - XCODE_VERSION: '16.4' - FLAKY_TESTS: keep_retrying - RUSTC_VERSION: '1.86' - -permissions: - contents: read - -jobs: - test-macOS: - if: github.event.pull_request.draft == false - strategy: - fail-fast: false - runs-on: macos-15 - env: - CC: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} gcc - CXX: ${{ (github.base_ref == 'main' || github.ref_name == 'main') && 'sccache' || '' }} g++ - SCCACHE_GHA_ENABLED: ${{ github.base_ref == 'main' || github.ref_name == 'main' }} - SCCACHE_IDLE_TIMEOUT: '0' - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - path: node - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Set up Xcode ${{ env.XCODE_VERSION }} - run: sudo xcode-select -s /Applications/Xcode_${{ env.XCODE_VERSION }}.app - - name: Install Rust ${{ env.RUSTC_VERSION }} - run: | - rustup override set "$RUSTC_VERSION" - rustup --version - - name: Set up sccache - if: github.base_ref == 'main' || github.ref_name == 'main' - uses: Mozilla-Actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - with: - version: v0.12.0 - - name: Environment Information - run: npx envinfo - # The `npm ci` for this step fails a lot as part of the Test step. Run it - # now so that we don't have to wait 2 hours for the Build step to pass - # first before that failure happens. (And if there's something about - # `make run-ci -j3` that is causing the failure and the failure doesn't - # happen anymore running this step here first, that's also useful - # information.) - - name: tools/doc/node_modules workaround - run: make -C node tools/doc/node_modules - # This is needed due to https://github.com/nodejs/build/issues/3878 - - name: Cleanup - run: | - echo "::group::Free space before cleanup" - df -h - echo "::endgroup::" - echo "::group::Cleaned Files" - - sudo rm -rf /Users/runner/Library/Android/sdk - - echo "::endgroup::" - echo "::group::Free space after cleanup" - df -h - echo "::endgroup::" - - name: Build - run: make -C node build-ci -j$(getconf _NPROCESSORS_ONLN) V=1 CONFIG_FLAGS="--error-on-warn --v8-enable-temporal-support" - - name: Free Space After Build - run: df -h - - name: Test - run: make -C node test-ci -j1 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9" - - name: Re-run test in a folder whose name contains unusual chars - run: | - mv node "$DIR" - cd "$DIR" - ./tools/test.py --flaky-tests keep_retrying -p actions -j 4 - env: - DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…` diff --git a/.github/workflows/tools.yml b/.github/workflows/tools.yml deleted file mode 100644 index 7966102b4676..000000000000 --- a/.github/workflows/tools.yml +++ /dev/null @@ -1,346 +0,0 @@ -name: Tools and deps update -on: - schedule: - # Run once a week at 00:05 AM UTC on Sunday. - - cron: 5 0 * * 0 - - workflow_dispatch: - inputs: - id: - description: The ID of the job to run - required: true - default: all - type: choice - options: - - all - - acorn - - acorn-walk - - ada - - amaro - - brotli - - c-ares - - merve - - corepack - - googletest - - gyp-next - - histogram - - icu - - inspector_protocol - - libffi - - libuv - - llhttp - - minimatch - - nbytes - - nixpkgs-unstable - - nghttp2 - - nghttp3 - - ngtcp2 - - postject - - root-certificates - - simdjson - - sqlite - - test426-fixtures - - undici - - uvwasi - - zlib - - zstd - -env: - PYTHON_VERSION: '3.14' - -permissions: - contents: read - -jobs: - tools-deps-update: - if: github.repository == 'nodejs/node' || github.event_name == 'workflow_dispatch' - # cannot use ubuntu-slim here because some update scripts require Docker - runs-on: ubuntu-latest - strategy: - fail-fast: false # Prevent other jobs from aborting if one fails - matrix: - include: - - id: acorn - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-acorn.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: acorn-walk - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-acorn-walk.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: ada - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-ada.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: amaro - subsystem: deps - label: dependencies, strip-types - run: | - ./tools/dep_updaters/update-amaro.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: brotli - subsystem: deps - label: dependencies, zlib - run: | - ./tools/dep_updaters/update-brotli.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: c-ares - subsystem: deps - label: dependencies, cares - run: | - ./tools/dep_updaters/update-c-ares.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: merve - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-merve.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: corepack - subsystem: deps - label: dependencies - run: | - make corepack-update - echo "NEW_VERSION=$(node deps/corepack/dist/corepack.js --version)" >> $GITHUB_ENV - - id: googletest - subsystem: deps - label: dependencies, test - run: | - ./tools/dep_updaters/update-googletest.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: gyp-next - subsystem: tools - label: tools, gyp - run: | - ./tools/dep_updaters/update-gyp-next.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: histogram - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-histogram.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: icu - subsystem: deps - label: dependencies, test, icu - run: | - ./tools/dep_updaters/update-icu.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: inspector_protocol - subsystem: deps - label: dependencies, inspector - run: | - ./tools/dep_updaters/update-inspector-protocol.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: libffi - subsystem: deps - label: dependencies, ffi - run: | - ./tools/dep_updaters/update-libffi.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: libuv - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-libuv.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: llhttp - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-llhttp.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: minimatch - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-minimatch.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: nbytes - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-nbytes.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: nixpkgs-unstable - subsystem: tools - # dont-land labels are there so we can guarantee released versions of - # Node.js can be built with the same env along the whole release line life – or - # at least we can detect and document necessary updates. - label: tools, dont-land-on-v22.x, dont-land-on-v24.x, dont-land-on-v26.x - run: | - ./tools/dep_updaters/update-nixpkgs-pin.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: nghttp2 - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-nghttp2.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: nghttp3 - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-nghttp3.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: ngtcp2 - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-ngtcp2.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: postject - subsystem: deps,test - label: test - run: | - ./tools/dep_updaters/update-postject.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: root-certificates - subsystem: crypto - label: crypto, notable-change - run: | - node ./tools/dep_updaters/update-root-certs.mjs -v -f "$GITHUB_ENV" - - id: simdjson - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-simdjson.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: sqlite - subsystem: deps - label: dependencies, sqlite - run: | - ./tools/dep_updaters/update-sqlite.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: test426-fixtures - subsystem: test - label: test - run: | - bash tools/dep_updaters/update-test426-fixtures.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: undici - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-undici.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: uvwasi - subsystem: deps - label: dependencies - run: | - ./tools/dep_updaters/update-uvwasi.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: zlib - subsystem: deps - label: dependencies, zlib - run: | - ./tools/dep_updaters/update-zlib.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - - id: zstd - subsystem: deps - label: dependencies, zlib - run: | - ./tools/dep_updaters/update-zstd.sh > temp-output - cat temp-output - tail -n1 temp-output | grep "NEW_VERSION=" >> "$GITHUB_ENV" || true - rm temp-output - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - if: github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id - with: - persist-credentials: false - - name: Set up Python ${{ env.PYTHON_VERSION }} - if: | - (matrix.id == 'icu' || matrix.id == 'inspector_protocol') && - (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - allow-prereleases: true - - name: Set up Nix - if: matrix.id == 'nixpkgs-unstable' && (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) - uses: cachix/install-nix-action@8aa03977d8d733052d78f4e008a241fd1dbf36b3 # v31.10.6 - - run: ${{ matrix.run }} - if: github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id - env: - GITHUB_TOKEN: ${{ secrets.GH_USER_TOKEN }} - - name: Generate commit message if not set - if: env.COMMIT_MSG == '' && (github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id) - run: | - echo "COMMIT_MSG=${{ matrix.subsystem }}: update ${{ matrix.id }} to ${{ env.NEW_VERSION }}" >> "$GITHUB_ENV" - - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 - if: github.event_name == 'schedule' || inputs.id == 'all' || inputs.id == matrix.id - # Creates a PR or update the Action's existing PR, or - # no-op if the base branch is already up-to-date. - with: - token: ${{ secrets.GH_USER_TOKEN }} - branch: actions/${{ github.ref_name == 'main' || format('{0}/', github.ref_name) }}tools-update-${{ matrix.id }} # Custom branch *just* for this Action. - delete-branch: true - commit-message: ${{ env.COMMIT_MSG }} - labels: ${{ matrix.label }} - title: '${{ matrix.subsystem }}: update ${{ matrix.id }} to ${{ env.NEW_VERSION }}' - body: This is an automated update of ${{ matrix.id }} to ${{ env.NEW_VERSION }}. - committer: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> - author: Node.js GitHub Bot diff --git a/.gitignore b/.gitignore index 69c1dd205316..3c98ecc2abf9 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ # === Rules for MSVS artifacts === Debug/ Release/ +!.github/actions/release *.sln *.suo *.vcxproj* @@ -86,6 +87,8 @@ _UpgradeReport_Files/ /deps/icu*.tgz /deps/icu-tmp ./node_modules +/replay-test/node_modules +/replay-test/package-lock.json /android-toolchain/ # generated by gyp on Windows /deps/openssl/openssl.props @@ -172,6 +175,8 @@ __pycache__ .DS_Store *~ +/src/node_record_replay_driver.cc + # === Rules for C++ development === compile_commands.json diff --git a/REPLAY_BACKEND_REV b/REPLAY_BACKEND_REV new file mode 100644 index 000000000000..43938271869c --- /dev/null +++ b/REPLAY_BACKEND_REV @@ -0,0 +1 @@ +d887c4838d6a4e305f874edd8cf3d422a068d9a2 \ No newline at end of file diff --git a/build.js b/build.js new file mode 100644 index 000000000000..95d769705416 --- /dev/null +++ b/build.js @@ -0,0 +1,146 @@ +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); +const node = __dirname; +const OutDir = path.join(node, "out"); + +// Use local driver directory if provided, otherwise download from S3. +const localDriverDir = process.env.REPLAY_LOCAL_DRIVER_DIR; +const driverFile = `${currentPlatform()}-recordreplay.${driverExtension()}`; +const driverJSON = `${currentPlatform()}-recordreplay.json`; + +let driverContents; +let driverRevision; +let driverDate; + +if (localDriverDir) { + console.log("[build] Loading driver from local directory:", localDriverDir); + driverContents = fs.readFileSync(path.join(localDriverDir, driverFile)); + const driverInfo = JSON.parse(fs.readFileSync(path.join(localDriverDir, driverJSON), "utf8")); + driverRevision = driverInfo.revision; + driverDate = driverInfo.date; +} else { + console.log("[build] Downloading driver from S3..."); + let driverArchive = `${currentPlatform()}-recordreplay.tgz`; + let downloadDriverRevision = process.env.DRIVER_REVISION ? process.env.DRIVER_REVISION : fs.readFileSync("REPLAY_BACKEND_REV", "utf8"); + let downloadArchive = `${currentPlatform()}-recordreplay-${downloadDriverRevision.trim().substring(0, 12)}.tgz`; + const driverArchivePath = path.join(OutDir, driverArchive); + spawnChecked("curl", [`https://static.replay.io/downloads/${downloadArchive}`, "-o", driverArchivePath], { stdio: "inherit" }); + spawnChecked("tar", ["xf", driverArchivePath, "-C", OutDir]); + fs.unlinkSync(driverArchivePath); + + driverContents = fs.readFileSync(path.join(OutDir, driverFile)); + const driverInfo = JSON.parse(fs.readFileSync(path.join(OutDir, driverJSON), "utf8")); + driverRevision = driverInfo.revision; + driverDate = driverInfo.date; + fs.unlinkSync(path.join(OutDir, driverFile)); + fs.unlinkSync(path.join(OutDir, driverJSON)); + fs.unlinkSync(path.join(OutDir, `${driverFile}.symbols.json`)); +} +console.log("[build] Generating driver source file..."); +let driverString = ""; +for (let i = 0; i < driverContents.length; i++) { + driverString += `\\${driverContents[i].toString(8)}`; +} +fs.writeFileSync( + `${node}/src/node_record_replay_driver.cc`, + ` +namespace node { + char gRecordReplayDriver[] = "${driverString}"; + int gRecordReplayDriverSize = ${driverContents.length}; + char gBuildId[] = "${computeBuildId()}"; +} +` +); + +const numCPUs = os.cpus().length; + +function getSanitizedEnv() { + const env = { ...process.env }; + if (env.PATH) { + env.PATH = env.PATH.split(":").filter(p => !p.includes("/nix/")).join(":"); + } + delete env.NIX_PROFILES; + delete env.NIX_SSL_CERT_FILE; + return env; +} + +const buildEnv = getSanitizedEnv(); + +if (process.env.CONFIGURE_NODE) { + console.log("[build] Running configure..."); + spawnChecked(`${node}/configure`, [], { cwd: node, stdio: "inherit", env: buildEnv }); +} +console.log("[build] Running make..."); +spawnChecked("make", [`-j${numCPUs}`, "-C", OutDir, "BUILDTYPE=Release"], { + cwd: node, + stdio: "inherit", + env: { + ...buildEnv, + RECORD_REPLAY_DONT_RECORD: "1", + }, +}); + +function spawnChecked(cmd, args, options) { + const prettyCmd = [cmd].concat(args).join(" "); + console.error(prettyCmd); + + const rv = spawnSync(cmd, args, options); + + if (rv.status != 0 || rv.error) { + console.error(rv.error); + throw new Error(`Spawned process failed with exit code ${rv.status}`); + } + + return rv; +} + +function currentPlatform() { + switch (process.platform) { + case "darwin": + return "macOS"; + case "linux": + return "linux"; + default: + throw new Error(`Platform ${process.platform} not supported`); + } +} + +function driverExtension() { + return currentPlatform() == "windows" ? "dll" : "so"; +} + +/** + * @returns {string} "YYYYMMDD" format of UTC timestamp of given revision. + */ +function getRevisionDate( + revision = "HEAD", + spawnOptions +) { + const dateString = spawnChecked( + "git", + ["show", revision, "--pretty=%cd", "--date=iso-strict", "--no-patch"], + spawnOptions + ) + .stdout.toString() + .trim(); + + // convert to UTC -> then get the date only + // explanations: https://github.com/replayio/backend/pull/7115#issue-1587869475 + return new Date(dateString).toISOString().substring(0, 10).replace(/-/g, ""); +} + +/** + * WARNING: We have copy-and-pasted `computeBuildId` into all our runtimes and `backend`. + * When changing this: always keep all versions of this in sync, or else, builds will break. + */ +function computeBuildId() { + const runtimeRevision = spawnChecked("git", ["rev-parse", "--short=12", "HEAD"]).stdout.toString().trim(); + const runtimeDate = getRevisionDate(); + + // Use the later of the two dates in the build ID. + const date = +runtimeDate >= +driverDate ? runtimeDate : driverDate; + + return `${currentPlatform()}-node-${date}-${runtimeRevision}-${driverRevision}`; +} diff --git a/deps/icu-small/source/common/umutex.cpp b/deps/icu-small/source/common/umutex.cpp index 0c053968dcc8..f3964cfcaaa0 100644 --- a/deps/icu-small/source/common/umutex.cpp +++ b/deps/icu-small/source/common/umutex.cpp @@ -51,8 +51,7 @@ std::condition_variable *initCondition; // Used when ICU implementation code passes nullptr for the mutex pointer. UMutex globalMutex; -std::once_flag initFlag; -std::once_flag *pInitFlag = &initFlag; +pthread_once_t initFlag; } // Anonymous namespace @@ -62,10 +61,7 @@ static UBool U_CALLCONV umtx_cleanup() { initCondition->~condition_variable(); UMutex::cleanup(); - // Reset the once_flag, by destructing it and creating a fresh one in its place. - // Do not use this trick anywhere else in ICU; use umtx_initOnce, not std::call_once(). - pInitFlag->~once_flag(); - pInitFlag = new(&initFlag) std::once_flag(); + initFlag = PTHREAD_ONCE_INIT; return true; } @@ -80,7 +76,7 @@ U_CDECL_END std::mutex *UMutex::getMutex() { std::mutex *retPtr = fMutex.load(std::memory_order_acquire); if (retPtr == nullptr) { - std::call_once(*pInitFlag, umtx_init); + pthread_once(&initFlag, umtx_init); std::lock_guard guard(*initMutex); retPtr = fMutex.load(std::memory_order_acquire); if (retPtr == nullptr) { @@ -143,7 +139,7 @@ umtx_unlock(UMutex* mutex) // U_COMMON_API UBool U_EXPORT2 umtx_initImplPreInit(UInitOnce &uio) { - std::call_once(*pInitFlag, umtx_init); + pthread_once(&initFlag, umtx_init); std::unique_lock lock(*initMutex); if (umtx_loadAcquire(uio.fState) == 0) { umtx_storeRelease(uio.fState, 1); diff --git a/deps/openssl/openssl/crypto/o_str.c b/deps/openssl/openssl/crypto/o_str.c index 154393c7b3e7..99d39d40af22 100644 --- a/deps/openssl/openssl/crypto/o_str.c +++ b/deps/openssl/openssl/crypto/o_str.c @@ -16,6 +16,8 @@ #include "internal/thread_once.h" #include "internal/to_hex.h" +extern void RecordReplayAssertFromC(const char* aFormat, ...); + #define DEFAULT_SEPARATOR ':' #define CH_ZERO '\0' @@ -370,6 +372,8 @@ char *OPENSSL_buf2hexstr(const unsigned char *buf, long buflen) return ossl_buf2hexstr_sep(buf, buflen, DEFAULT_SEPARATOR); } + RecordReplayAssertFromC("openssl_strerror_r %d %lu", errnum, buflen); + int openssl_strerror_r(int errnum, char *buf, size_t buflen) { #if defined(_MSC_VER) && _MSC_VER >= 1400 && !defined(_WIN32_WCE) diff --git a/deps/openssl/openssl/crypto/rand/rand_lib.c b/deps/openssl/openssl/crypto/rand/rand_lib.c index f03bb2967c43..17b55955f6ea 100644 --- a/deps/openssl/openssl/crypto/rand/rand_lib.c +++ b/deps/openssl/openssl/crypto/rand/rand_lib.c @@ -133,6 +133,8 @@ static int set_random_provider_name(RAND_GLOBAL *dgbl, const char *name) return dgbl->random_provider_name != NULL; } +extern void RecordReplayAssertFromC(const char* aFormat, ...); + #ifndef OPENSSL_NO_ENGINE /* non-NULL if default_RAND_meth is ENGINE-provided */ static ENGINE *funct_ref; @@ -145,6 +147,14 @@ static const RAND_METHOD *default_RAND_meth; static CRYPTO_ONCE rand_init = CRYPTO_ONCE_STATIC_INIT; static int rand_inited = 0; + RecordReplayAssertFromC("rand_drbg_get_entropy #3"); + + RecordReplayAssertFromC("rand_drbg_get_entropy #1 %d", !!drbg->parent); + + RecordReplayAssertFromC("rand_drbg_get_entropy #2 %d", !!buffer); + + RecordReplayAssertFromC("rand_drbg_get_entropy %d", !!drbg->parent); + DEFINE_RUN_ONCE_STATIC(do_rand_init) { diff --git a/deps/uv/include/uv.h b/deps/uv/include/uv.h index da06345557bb..f53e38b6cde0 100644 --- a/deps/uv/include/uv.h +++ b/deps/uv/include/uv.h @@ -1850,6 +1850,7 @@ UV_EXTERN void uv_mutex_destroy(uv_mutex_t* handle); UV_EXTERN void uv_mutex_lock(uv_mutex_t* handle); UV_EXTERN int uv_mutex_trylock(uv_mutex_t* handle); UV_EXTERN void uv_mutex_unlock(uv_mutex_t* handle); +UV_EXTERN void uv_mutex_mark_ordered(uv_mutex_t* handle); UV_EXTERN int uv_rwlock_init(uv_rwlock_t* rwlock); UV_EXTERN void uv_rwlock_destroy(uv_rwlock_t* rwlock); diff --git a/deps/uv/include/uv/unix.h b/deps/uv/include/uv/unix.h index c6ba419da98b..8a2027e029e0 100644 --- a/deps/uv/include/uv/unix.h +++ b/deps/uv/include/uv/unix.h @@ -325,6 +325,7 @@ typedef struct { uv_async_cb async_cb; \ struct uv__queue queue; \ int pending; \ + int ordered_lock_id; #define UV_TIMER_PRIVATE_FIELDS \ uv_timer_cb timer_cb; \ diff --git a/deps/uv/src/threadpool.c b/deps/uv/src/threadpool.c index 2a129a5d4a58..4d41976eda08 100644 --- a/deps/uv/src/threadpool.c +++ b/deps/uv/src/threadpool.c @@ -29,6 +29,8 @@ #define MAX_THREADPOOL_SIZE 1024 +extern void V8RecordReplayAssert(const char* format, ...); + static uv_once_t once = UV_ONCE_INIT; static uv_cond_t cond; static uv_mutex_t mutex; @@ -227,6 +229,7 @@ static void init_threads(void) { if (uv_cond_init(&cond)) abort(); + uv_mutex_mark_ordered(&mutex); if (uv_mutex_init(&mutex)) abort(); @@ -324,6 +327,8 @@ void uv__work_done(uv_async_t* handle) { int err; int nevents; + V8RecordReplayAssert("uv__work_done"); + loop = container_of(handle, uv_loop_t, wq_async); uv_mutex_lock(&loop->wq_mutex); uv__queue_move(&loop->wq, &wq); diff --git a/deps/uv/src/unix/darwin.c b/deps/uv/src/unix/darwin.c index c26a1faccbcc..8a684e487d7b 100644 --- a/deps/uv/src/unix/darwin.c +++ b/deps/uv/src/unix/darwin.c @@ -32,6 +32,11 @@ #include #include /* sysconf */ +extern int V8RecordReplayIsReplaying(void); +extern uintptr_t V8RecordReplayValue(const char* why, uintptr_t value); +extern void V8RecordReplayBeginPassThroughEvents(void); +extern void V8RecordReplayEndPassThroughEvents(void); + static uv_once_t once = UV_ONCE_INIT; static mach_timebase_info_data_t timebase; @@ -205,7 +210,20 @@ int uv_cpu_info(uv_cpu_info_t** cpu_infos, int* count) { return UV__ERR(errno); } - cpuspeed = 0; + // Some of the system interactions in uv__get_cpu_speed are not supported + // while recording, so we pass through events and manually record/replay + // the values we get from this call. + if (!V8RecordReplayIsReplaying()) { + V8RecordReplayBeginPassThroughEvents(); + err = uv__get_cpu_speed(&cpuspeed); + V8RecordReplayEndPassThroughEvents(); + V8RecordReplayValue("uv__get_cpu_speed err", err); + V8RecordReplayValue("uv__get_cpu_speed cpuspeed", cpuspeed); + } else { + err = V8RecordReplayValue("uv__get_cpu_speed err", 0); + cpuspeed = V8RecordReplayValue("uv__get_cpu_speed cpuspeed", 0); + } + size = sizeof(cpuspeed); sysctlbyname("hw.cpufrequency", &cpuspeed, &size, NULL, 0); if (cpuspeed == 0) diff --git a/deps/uv/src/unix/kqueue.c b/deps/uv/src/unix/kqueue.c index 27d288143633..c644a6722ce1 100644 --- a/deps/uv/src/unix/kqueue.c +++ b/deps/uv/src/unix/kqueue.c @@ -48,6 +48,8 @@ #define EV_OOBAND EV_FLAG1 #endif +extern void V8RecordReplayAssert(const char* format, ...); + int uv__kqueue_init(uv_loop_t* loop) { loop->backend_fd = kqueue(); @@ -179,6 +181,8 @@ void uv__io_poll(uv_loop_t* loop, int timeout) { int user_timeout; int reset_timeout; + V8RecordReplayAssert("uv__io_poll"); + if (loop->nfds == 0) { assert(uv__queue_empty(&loop->watcher_queue)); return; diff --git a/deps/uv/src/unix/loop.c b/deps/uv/src/unix/loop.c index da9fa3c8b80b..0a6268ec4f62 100644 --- a/deps/uv/src/unix/loop.c +++ b/deps/uv/src/unix/loop.c @@ -93,6 +93,7 @@ int uv_loop_init(uv_loop_t* loop) { err = uv_mutex_init(&loop->wq_mutex); if (err) goto fail_mutex_init; + uv_mutex_mark_ordered(&loop->wq_mutex); err = uv_async_init(loop, &loop->wq_async, uv__work_done); if (err) diff --git a/deps/uv/src/unix/process.c b/deps/uv/src/unix/process.c index 539e7d9417d3..f7f1b43c2bd6 100644 --- a/deps/uv/src/unix/process.c +++ b/deps/uv/src/unix/process.c @@ -70,6 +70,8 @@ extern char **environ; #define UV_USE_SIGCHLD #endif +extern void V8RecordReplayAssert(const char* format, ...); +extern void V8RecordReplayDiagnostic(const char* format, ...); #ifdef UV_USE_SIGCHLD static void uv__chld(uv_signal_t* handle, int signum) { @@ -109,6 +111,8 @@ void uv__wait_children(uv_loop_t* loop) { struct uv__queue* q; struct uv__queue* h; + V8RecordReplayAssert("uv__chld start"); + uv__queue_init(&pending); h = &loop->process_handles; @@ -288,6 +292,8 @@ static void uv__process_child_init(const uv_process_options_t* options, int stdio_count, int (*pipes)[2], int error_fd) { + V8RecordReplayDiagnostic("uv__process_child_init start"); + sigset_t signewset; int close_fd; int use_fd; @@ -934,9 +940,11 @@ static int uv__spawn_and_init_child( uv__close(signal_pipe[1]); if (err == 0) { - do - r = read(signal_pipe[0], &exec_errorno, sizeof(exec_errorno)); - while (r == -1 && errno == EINTR); + do { + V8RecordReplayDiagnostic("uv_spawn read pipe start"); + r = read(signal_pipe[0], &exec_errorno, sizeof(exec_errorno)); + V8RecordReplayDiagnostic("uv_spawn read pipe done %d %d", r, errno); + } while (r == -1 && errno == EINTR); if (r == 0) ; /* okay, EOF */ @@ -978,6 +986,12 @@ int uv_spawn(uv_loop_t* loop, int exec_errorno; int i; + V8RecordReplayDiagnostic("uv_spawn start"); + char** nargs; + for (nargs = options->args; *nargs; nargs++) { + V8RecordReplayDiagnostic("uv_spawn arg %s", *nargs); + } + assert(options->file != NULL); assert(!(options->flags & ~(UV_PROCESS_DETACHED | UV_PROCESS_SETGID | diff --git a/deps/uv/src/unix/proctitle.c b/deps/uv/src/unix/proctitle.c index 9d1f00ddf66e..d07dc7f8cf2c 100644 --- a/deps/uv/src/unix/proctitle.c +++ b/deps/uv/src/unix/proctitle.c @@ -97,6 +97,11 @@ int uv_set_process_title(const char* title) { struct uv__process_title* pt; size_t len; + // FIXME disabling for now. When replaying the arguments are not adjacent + // and the logic in uv_setup_args behaves incorrectly and corrupts most + // of the process memory. + return 0; + /* If uv_setup_args wasn't called or failed, we can't continue. */ if (args_mem == NULL) return UV_ENOBUFS; diff --git a/deps/uv/src/unix/signal.c b/deps/uv/src/unix/signal.c index 91fb149464e3..6bb009554deb 100644 --- a/deps/uv/src/unix/signal.c +++ b/deps/uv/src/unix/signal.c @@ -32,6 +32,15 @@ # define SA_RESTART 0 #endif +extern int V8RecordReplayIsRecording(void); +extern int V8RecordReplayIsReplaying(void); +extern void V8RecordReplayAssert(const char* format, ...); +extern void V8RecordReplayDiagnostic(const char* format, ...); +extern uintptr_t V8RecordReplayValue(const char* why, uintptr_t value); +extern void V8RecordReplayRegisterPointer(const void* ptr); +extern int V8RecordReplayPointerId(const void* ptr); +extern void* V8RecordReplayIdPointer(int id); + typedef struct { uv_signal_t* handle; int signum; @@ -426,6 +435,10 @@ static int uv__signal_start(uv_signal_t* handle, RB_INSERT(uv__signal_tree_s, &uv__signal_tree, handle); + // Note: Handles added to the tree are not unregistered, which is a minor + // memory leak but is otherwise fine. + V8RecordReplayRegisterPointer(handle); + uv__signal_unlock_and_unblock(&saved_sigmask); handle->signal_cb = signal_cb; @@ -476,6 +489,27 @@ void uv__signal_event(uv_loop_t* loop, uv__io_t* w, unsigned int events) { msg = (uv__signal_msg_t*) (buf + i); handle = msg->handle; + /* When recording/replaying, the data we read from the pipe will be the + * data which was originally read from the pipe while recording, because + * the write to the pipe happened inside the signal handler and is not + * replayed. + * + * Because of this the handle pointer we have here is invalid when + * replaying, and we need to save/restore the actual pointer we can use + * in this process. + */ + if (V8RecordReplayIsRecording()) { + int id = V8RecordReplayPointerId(handle); + if (!id) { + fprintf(stderr, "uv__signal_event: Missing pointer ID for message handle\n"); + abort(); + } + V8RecordReplayValue("uv__signal_event handle", id); + } else if (V8RecordReplayIsReplaying()) { + int id = V8RecordReplayValue("uv__signal_event handle", 0); + handle = V8RecordReplayIdPointer(id); + } + if (msg->signum == handle->signum) { assert(!(handle->flags & UV_HANDLE_CLOSING)); handle->signal_cb(handle, handle->signum); diff --git a/deps/uv/src/unix/stream.c b/deps/uv/src/unix/stream.c index 08a18314b1ec..953ceeefeacc 100644 --- a/deps/uv/src/unix/stream.c +++ b/deps/uv/src/unix/stream.c @@ -70,6 +70,8 @@ union uv__cmsg { STATIC_ASSERT(256 == sizeof(union uv__cmsg)); +extern void V8RecordReplayAssert(const char* format, ...); + static void uv__stream_connect(uv_stream_t*); static void uv__write(uv_stream_t* stream); static void uv__read(uv_stream_t* stream); @@ -766,6 +768,12 @@ static int uv__try_write(uv_stream_t* stream, iov = (struct iovec*) bufs; iovcnt = nbufs; + // https://linear.app/replay/issue/RUN-650 + V8RecordReplayAssert("uv__try_write %d %u", uv__stream_fd(stream), nbufs); + if (nbufs == 1) { + V8RecordReplayAssert("uv__try_write #1 %u", iov[0].iov_len); + } + iovmax = uv__getiovmax(); /* Limit iov count to avoid EINVALs from writev() */ @@ -1021,6 +1029,7 @@ static int uv__stream_recv_cmsg(uv_stream_t* stream, struct msghdr* msg) { } + static void uv__read(uv_stream_t* stream) { uv_buf_t buf; ssize_t nread; @@ -1058,6 +1067,7 @@ static void uv__read(uv_stream_t* stream) { if (!is_ipc) { do { + V8RecordReplayAssert("uv__read read %d %lu", uv__stream_fd(stream), buf.len); nread = read(uv__stream_fd(stream), buf.base, buf.len); } while (nread < 0 && errno == EINTR); diff --git a/deps/uv/src/unix/thread.c b/deps/uv/src/unix/thread.c index 001b6d744ba8..39b851204756 100644 --- a/deps/uv/src/unix/thread.c +++ b/deps/uv/src/unix/thread.c @@ -387,6 +387,14 @@ void uv_mutex_unlock(uv_mutex_t* mutex) { } +extern void V8RecordReplayAddOrderedPthreadMutex(const char* name, + pthread_mutex_t* mutex); + +void uv_mutex_mark_ordered(uv_mutex_t* mutex) { + V8RecordReplayAddOrderedPthreadMutex("UVMutex", mutex); +} + + int uv_rwlock_init(uv_rwlock_t* rwlock) { return UV__ERR(pthread_rwlock_init(rwlock, NULL)); } diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index bde4cb2be84b..99612616de18 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -166,6 +166,50 @@ const rawMethods = internalBinding('process_methods'); process.reallyExit = rawMethods.reallyExit; process._kill = rawMethods._kill; + const recordreplay = { + log: rawMethods.recordReplayLog, + assert: rawMethods.recordReplayAssert, + recordingId: rawMethods.recordReplayRecordingId, + currentPoint: rawMethods.recordReplayCurrentExecutionPoint, + elapsedTime: rawMethods.recordReplayElapsedTimeMs, + annotationHook: rawMethods.recordReplayAnnotationHook, + + currentPointURL() { + const recordingId = rawMethods.recordReplayRecordingId(); + const point = rawMethods.recordReplayCurrentExecutionPoint(); + const time = rawMethods.recordReplayElapsedTimeMs(); + return `https://app.replay.io/recording/${recordingId}?point=${point}&hasFrames=true&time=${time}`; + }, + + _onConsoleAPI: rawMethods.recordReplayOnConsoleAPI, + _ignoreScript: rawMethods.recordReplayIgnoreScript, + _setCommandCallback: rawMethods.recordReplaySetCommandCallback, + _setClearPauseDataCallback: rawMethods.recordReplaySetClearPauseDataCallback, + _setCDPMessageCallback: rawMethods.recordReplaySetCDPMessageCallback, + _sendCDPMessage: rawMethods.recordReplaySendCDPMessage, + _getCurrentError: rawMethods.recordReplayGetCurrentError, + }; + + ObjectDefineProperty(process, "recordreplay", { + get() { + if (rawMethods.isRecordingOrReplaying()) { + return recordreplay; + } + }, + // If the user tries to set this to another value, override + // this completely to that value. + set(value) { + ObjectDefineProperty(this, "recordreplay", { + value, + configurable: true, + enumerable: true, + writable: true + }); + }, + enumerable: true, + configurable: true + }); + const wrapped = perThreadSetup.wrapProcessMethods(rawMethods); process.loadEnvFile = wrapped.loadEnvFile; process._rawDebug = wrapped._rawDebug; diff --git a/lib/internal/console/constructor.js b/lib/internal/console/constructor.js index bb79678001ec..0f398db74988 100644 --- a/lib/internal/console/constructor.js +++ b/lib/internal/console/constructor.js @@ -400,6 +400,21 @@ function createWriteErrorHandler(instance, streamSymbol) { }; } +// Note: the "this" value for this call is the console message arguments, +// which Target_getCurrentMessageContents will read via the inspector. +function onConsoleMessage() { + if (process.recordreplay) { + process.recordreplay._onConsoleAPI(); + } +} + + onConsoleMessage.call(args); + onConsoleMessage.call(args); + this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args)); + }, + + error(...args) { + onConsoleMessage.call(args); function timeLogImpl(consoleRef, label, formatted, args) { if (args === undefined) { consoleRef.log('%s: %s', label, formatted); @@ -663,7 +678,6 @@ const consoleMethods = { return final(keys, values); }, -}; const keyKey = 'Key'; const valuesKey = 'Values'; diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index 38ea6675928a..e7c78a4c5df9 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -248,6 +248,9 @@ function initializeModuleLoaders(options) { } // Need to be done after --require setup. initializeFrozenIntrinsics(); + + const { initializeRecordReplay } = require('internal/recordreplay/main'); + initializeRecordReplay(); } function refreshRuntimeOptions() { diff --git a/lib/internal/recordreplay/main.js b/lib/internal/recordreplay/main.js new file mode 100644 index 000000000000..ff56d2023016 --- /dev/null +++ b/lib/internal/recordreplay/main.js @@ -0,0 +1,304 @@ +'use strict'; + +// Methods for interacting with the record/replay driver. + +const { + initMessages, + sendMessage, + addEventListener, +} = require("internal/recordreplay/message"); +const { + remoteObjectToProtocolValue, + clearPauseDataCallback, + protocolIdToRemoteObject, +} = require("internal/recordreplay/object"); +const { + createProtocolObject, + createProtocolFrame, + createProtocolLocation, + createProtocolScope, +} = require("internal/recordreplay/preview"); +const { + registerSourceMap, + processSourceMaps, + getSourceMapURL +} = require("internal/recordreplay/sourcemap"); +const { assert, log } = require("internal/recordreplay/utils"); + +function initializeRecordReplay() { + if (process.recordreplay) { + initMessages(); + process.recordreplay._setCommandCallback(commandCallback); + process.recordreplay._setClearPauseDataCallback(clearPauseDataCallback); + addEventListener("Runtime.consoleAPICalled", onConsoleAPICall); + addEventListener("Debugger.scriptParsed", registerSourceMap); + process.on("exit", processSourceMaps); + sendMessage("Runtime.enable"); + sendMessage("Debugger.enable"); + } +} + +const CommandCallbacks = { + "Target.countStackFrames": Target_countStackFrames, + "Target.evaluatePrivileged": Target_evaluatePrivileged, + "Target.getCurrentMessageContents": Target_getCurrentMessageContents, + "Target.getSourceMapURL": Target_getSourceMapURL, + "Target.getStepOffsets": Target_getStepOffsets, + "Target.topFrameLocation": Target_topFrameLocation, + "Pause.evaluateInFrame": Pause_evaluateInFrame, + "Pause.evaluateInGlobal": Pause_evaluateInGlobal, + "Pause.getAllFrames": Pause_getAllFrames, + "Pause.getExceptionValue": Pause_getExceptionValue, + "Pause.getObjectPreview": Pause_getObjectPreview, + "Pause.getObjectProperty": Pause_getObjectProperty, + "Pause.getScope": Pause_getScope, +}; + +function commandCallback(method, params) { + if (!CommandCallbacks[method]) { + log(`Missing command callback: ${method}`); + return {}; + } + + try { + return CommandCallbacks[method](params); + } catch (e) { + log(`Error: Command exception ${method} ${e}`); + return {}; + } +} + +function Target_countStackFrames() { + const count = getStackFrames().length; + return { count }; +} + +function Target_evaluatePrivileged({ expression }) { + const result = eval(expression); + return { result }; +} + +// Contents of the last console API call. Runtime.consoleAPICalled will be +// emitted before the driver gets the current message contents. +let gLastConsoleAPICall; + +function onConsoleAPICall(params) { + gLastConsoleAPICall = params; +} + +function Target_getCurrentMessageContents() { + // We could be getting the contents of either an error object that was reported + // to the driver via C++, or a console API call that was reported to the driver + // via onConsoleAPICall(). + const error = process.recordreplay._getCurrentError(); + + if (error) { + const { message, filename, line, column, scriptId } = error; + return { + source: "PageError", + level: "error", + text: message, + url: filename, + sourceId: scriptId ? scriptId.toString() : undefined, + line, + column, + }; + } + + // Look for the "args" variable on an onConsoleMessage frame. + // The arguments are also stored on the last console API call, though + // if we use that we need to be careful because the pause state could have + // been cleared since the last Runtime.consoleAPICalled event. + const { callFrames } = sendMessage("Debugger.getCallFrames"); + const consoleMessageFrame = callFrames.find( + frame => frame.functionName == "onConsoleMessage" + ); + assert(consoleMessageFrame); + assert(consoleMessageFrame.this.type == "object"); + assert(consoleMessageFrame.this.className == "Array"); + const argumentsId = consoleMessageFrame.this.objectId; + + // Get the properties of the message arguments array. + const argumentsProperties = sendMessage("Runtime.getProperties", { + objectId: argumentsId, + ownProperties: true, + generatePreview: false, + }).result; + + // Get the protocol representation of the message arguments. + const argumentValues = []; + for (let i = 0;; i++) { + const property = argumentsProperties.find(prop => prop.name == i.toString()); + if (!property) { + break; + } + argumentValues.push(remoteObjectToProtocolValue(property.value)); + } + + let level = "info"; + switch (gLastConsoleAPICall.level) { + case "warning": + level = "warning"; + break; + case "error": + level = "error"; + break; + } + + let url, sourceId, line, column; + if (gLastConsoleAPICall.stackTrace) { + const frame = gLastConsoleAPICall.stackTrace.callFrames[0]; + if (frame) { + url = frame.url; + sourceId = frame.scriptId; + line = frame.lineNumber; + column = frame.columnNumber; + } + } + + return { + source: "ConsoleAPI", + level, + text: "", + url, + sourceId, + line, + column, + argumentValues, + }; +} + +function Target_getSourceMapURL({ sourceId }) { + const url = getSourceMapURL(sourceId); + return { url, baseUrl: url }; +} + +function Target_getStepOffsets() { + // CDP does not distinguish between steps and breakpoints. + return {}; +} + +function Target_topFrameLocation() { + // Instead of asking for the locations of all frames initially, see if we can + // find a frame that isn't being ignored near the top, so we don't have to walk + // the entire stack. + const InitialMaxFrames = 10; + const { frameLocations } = sendMessage("Debugger.getFrameLocations", { maxFrames: InitialMaxFrames }); + + for (const location of frameLocations) { + if (!process.recordreplay._ignoreScript(location.scriptId)) { + return { location: createProtocolLocation(location)[0] }; + } + } + + if (frameLocations.length == InitialMaxFrames) { + const { frameLocations: allFrameLocations } = sendMessage("Debugger.getFrameLocations"); + for (const location of allFrameLocations) { + if (!process.recordreplay._ignoreScript(location.scriptId)) { + return { location: createProtocolLocation(location)[0] }; + } + } + } + + return {}; +} + +// Get the raw call frames on the stack, eliding ones in scripts we are ignoring. +function getStackFrames() { + const { callFrames } = sendMessage("Debugger.getCallFrames"); + + const frames = []; + for (const frame of callFrames) { + if (!process.recordreplay._ignoreScript(frame.location.scriptId)) { + frames.push(frame); + } + } + return frames; +} + +// Build a protocol Result object from a result/exceptionDetails CDP rval. +function buildProtocolResult({ result, exceptionDetails }) { + const value = remoteObjectToProtocolValue(result); + const protocolResult = { data: {} }; + + if (exceptionDetails) { + protocolResult.exception = value; + } else { + protocolResult.returned = value; + } + return { result: protocolResult }; +} + +function Pause_evaluateInFrame({ frameId, expression }) { + const frames = getStackFrames(); + const index = +frameId; + assert(index < frames.length); + const frame = frames[index]; + + const rv = doEvaluation(); + return buildProtocolResult(rv); + + function doEvaluation() { + // In order to do the evaluation in the right frame, the same number of + // frames need to be on V8's stack when we do the evaluation as when we got + // the stack frames in the first place. The debugger agent extracts a frame + // index from the ID it is given and uses that to walk the stack to the + // frame where it will do the evaluation (see DebugStackTraceIterator). + return sendMessage( + "Debugger.evaluateOnCallFrame", + { + callFrameId: frame.callFrameId, + expression, + } + ); + } +} + +function Pause_evaluateInGlobal({ expression }) { + const rv = sendMessage("Runtime.evaluate", { expression }); + return buildProtocolResult(rv); +} + +function Pause_getAllFrames() { + const frames = getStackFrames().map((frame, index) => { + // Use our own IDs for frames. + const id = (index++).toString(); + return createProtocolFrame(id, frame); + }); + + return { + frames: frames.map(f => f.frameId), + data: { frames }, + }; +} + +function Pause_getExceptionValue() { + const rv = sendMessage("Debugger.getPendingException", {}); + return { exception: remoteObjectToProtocolValue(rv.exception), data: {} }; +} + +function Pause_getObjectPreview({ object, level = "full" }) { + const objectData = createProtocolObject(object, level); + return { data: { objects: [objectData] } }; +} + +function Pause_getObjectProperty({ object, name }) { + const obj = protocolIdToRemoteObject(object); + const rv = sendMessage( + "Runtime.callFunctionOn", + { + functionDeclaration: `function() { return this["${name}"] }`, + objectId: obj.objectId, + } + ); + return buildProtocolResult(rv); +} + +function Pause_getScope({ scope }) { + const scopeData = createProtocolScope(scope); + return { data: { scopes: [scopeData] } }; +} + +module.exports = { + initializeRecordReplay, +}; diff --git a/lib/internal/recordreplay/message.js b/lib/internal/recordreplay/message.js new file mode 100644 index 000000000000..91d87d066c5f --- /dev/null +++ b/lib/internal/recordreplay/message.js @@ -0,0 +1,50 @@ +// Interface for sending/receiving CDP messages. + +const { log, assert } = require("internal/recordreplay/utils"); + +function initMessages() { + process.recordreplay._setCDPMessageCallback(messageCallback); +} + +let gNextMessageId = 1; + +let gCurrentMessageId; +let gCurrentMessageResult; + +function sendMessage(method, params) { + const id = gNextMessageId++; + gCurrentMessageId = id; + process.recordreplay._sendCDPMessage(JSON.stringify({ method, params, id })); + gCurrentMessageId = undefined; + return gCurrentMessageResult; +} + +const gEventListeners = new Map(); + +function addEventListener(method, callback) { + gEventListeners.set(method, callback); +} + +function messageCallback(message) { + try { + message = JSON.parse(message); + + if (message.id) { + assert(message.id == gCurrentMessageId); + gCurrentMessageResult = message.result; + } else { + const listener = gEventListeners.get(message.method); + if (listener) { + listener(message.params); + } + } + } catch (e) { + log(`Message callback exception: ${e}`); + } +} + +module.exports = { + initMessages, + sendMessage, + addEventListener, +}; diff --git a/lib/internal/recordreplay/object.js b/lib/internal/recordreplay/object.js new file mode 100644 index 000000000000..5ded9a51d890 --- /dev/null +++ b/lib/internal/recordreplay/object.js @@ -0,0 +1,102 @@ +// Manage association between remote objects and protocol object IDs. + +const { assert, log } = require("internal/recordreplay/utils"); + +// Map protocol ObjectId => RemoteObject +const gProtocolIdToObject = new Map(); + +// Map RemoteObject.objectId => protocol ObjectId +const gObjectIdToProtocolId = new Map(); + +// Map protocol ScopeId => Debugger.Scope +const gProtocolIdToScope = new Map(); + +let gNextObjectId = 1; + +function clearPauseDataCallback() { + gProtocolIdToObject.clear(); + gObjectIdToProtocolId.clear(); + gProtocolIdToScope.clear(); + gNextObjectId = 1; +} + +function remoteObjectToProtocolId(remoteObject) { + assert(remoteObject.objectId); + + const existing = gObjectIdToProtocolId.get(remoteObject.objectId); + if (existing) { + return existing; + } + + const protocolObjectId = (gNextObjectId++).toString(); + gObjectIdToProtocolId.set(remoteObject.objectId, protocolObjectId); + gProtocolIdToObject.set(protocolObjectId, remoteObject); + + return protocolObjectId; +} + +function protocolIdToRemoteObject(objectId) { + const remoteObject = gProtocolIdToObject.get(objectId); + assert(remoteObject); + return remoteObject; +} + +// Strings longer than this will be truncated when creating protocol values. +const MaxStringLength = 10000; + +function remoteObjectToProtocolValue(obj) { + switch (obj.type) { + case "undefined": + return {}; + case "string": + case "number": + case "boolean": + if (obj.unserializableValue) { + assert(obj.type == "number"); + return { unserializableNumber: obj.unserializableValue }; + } + if (typeof obj.value == "string" && obj.value.length > MaxStringLength) { + return { value: obj.value.substring(0, MaxStringLength) + "…" }; + } + return { value: obj.value }; + case "bigint": { + const str = obj.unserializableValue; + assert(str); + return { bigint: str.substring(0, str.length - 1) }; + } + case "object": + case "function": { + if (!obj.objectId) { + return { value: null }; + } + const object = remoteObjectToProtocolId(obj); + return { object }; + } + case "symbol": + return { symbol: obj.description }; + default: + return { unavailable: true }; + } +} + +function scopeToProtocolId(scope) { + // Use the scope object's ID as the ID for the scope itself. + const id = remoteObjectToProtocolId(scope.object); + gProtocolIdToScope.set(id, scope); + return id; +} + +function protocolIdToScope(scopeId) { + const scope = gProtocolIdToScope.get(scopeId); + assert(scope); + return scope; +} + +module.exports = { + remoteObjectToProtocolId, + protocolIdToRemoteObject, + remoteObjectToProtocolValue, + scopeToProtocolId, + protocolIdToScope, + clearPauseDataCallback, +}; diff --git a/lib/internal/recordreplay/preview.js b/lib/internal/recordreplay/preview.js new file mode 100644 index 000000000000..443fb22a7cfb --- /dev/null +++ b/lib/internal/recordreplay/preview.js @@ -0,0 +1,359 @@ +// Logic for creating object previews for the record/replay protocol. + +const { + protocolIdToRemoteObject, + remoteObjectToProtocolId, + remoteObjectToProtocolValue, + scopeToProtocolId, + protocolIdToScope, +} = require("internal/recordreplay/object"); +const { sendMessage } = require("internal/recordreplay/message"); +const { log } = require("internal/recordreplay/utils"); + +function createProtocolObject(objectId, level) { + const obj = protocolIdToRemoteObject(objectId); + const className = obj.subtype == "proxy" ? "Proxy" : (obj.className || "Function"); + + let preview; + if (level != "none") { + preview = new ProtocolObjectPreview(obj, level).fill(); + } + + return { objectId, className, preview }; +} + +// Target limit for the number of items (properties etc.) to include in object +// previews before overflowing. +const MaxItems = { + "noProperties": 0, + + // Note: this is higher than on gecko-dev because typed arrays don't render + // properly in the devtools currently unless we include a minimum number of + // properties. This would be nice to fix. + "canOverflow": 10, + + "full": 1000, +}; + +function ProtocolObjectPreview(obj, level) { + this.obj = obj; + this.level = level; + this.overflow = false; + this.numItems = 0; + this.extra = {}; +} + +ProtocolObjectPreview.prototype = { + canAddItem(force) { + if (!force && this.numItems >= MaxItems[this.level]) { + this.overflow = true; + return false; + } + this.numItems++; + return true; + }, + + addProperty(property, force) { + if (!this.canAddItem(force)) { + return; + } + if (!this.properties) { + this.properties = []; + } + this.properties.push(property); + }, + + addContainerEntry(entry) { + if (!this.canAddItem()) { + return; + } + if (!this.containerEntries) { + this.containerEntries = []; + } + this.containerEntries.push(entry); + }, + + fill() { + const allProperties = sendMessage("Runtime.getProperties", { + objectId: this.obj.objectId, + ownProperties: true, + generatePreview: false, + }); + const properties = allProperties.result; + + // Add class-specific data. + const previewer = CustomPreviewers[this.obj.className]; + const requiredProperties = []; + if (previewer) { + for (const entry of previewer) { + if (typeof entry == "string") { + requiredProperties.push(entry); + } else { + entry.call(this, allProperties); + } + } + } + + let prototype; + for (const prop of properties) { + if (prop.name == "__proto__") { + prototype = prop; + } else { + const protocolProperty = createProtocolPropertyDescriptor(prop); + const force = requiredProperties.includes(prop.name); + this.addProperty(protocolProperty, force); + } + } + + let prototypeId; + if (prototype && prototype.value && prototype.value.objectId) { + prototypeId = remoteObjectToProtocolId(prototype.value); + } + return { + prototypeId, + overflow: (this.overflow && this.level != "full") ? true : undefined, + properties: this.properties, + containerEntries: this.containerEntries, + ...this.extra, + }; + }, +}; + +// Get a count from an object description like "Array(42)" +function getDescriptionCount(description) { + const match = /\((\d+)\)/.exec(description || ""); + if (match) { + return +match[1]; + } +} + +function previewTypedArray() { + // The typed array size isn't available from the object's own property + // information, except by parsing the object description. + const length = getDescriptionCount(this.obj.description); + if (length !== undefined) { + this.addProperty({ name: "length", value: length }, /* force */ true); + } +} + +function previewSetMap(allProperties) { + if (!allProperties.internalProperties) { + return; + } + + const internal = allProperties.internalProperties.find(prop => prop.name == "[[Entries]]"); + if (!internal || !internal.value || !internal.value.objectId) { + return; + } + + // Get the container size from the length of the entries. + const size = getDescriptionCount(internal.value.description); + if (size !== undefined) { + this.extra.containerEntryCount = size; + if (["Set", "Map"].includes(this.obj.className)) { + this.addProperty({ name: "size", value: size }, /* force */ true); + } + } + + const entries = sendMessage("Runtime.getProperties", { + objectId: internal.value.objectId, + ownProperties: true, + generatePreview: false, + }).result; + + for (const entry of entries) { + if (entry.value.subtype == "internal#entry") { + const entryProperties = sendMessage("Runtime.getProperties", { + objectId: entry.value.objectId, + ownProperties: true, + generatePreview: false, + }).result; + const key = entryProperties.find(eprop => eprop.name == "key"); + const value = entryProperties.find(eprop => eprop.name == "value"); + if (value) { + this.addContainerEntry({ + key: key ? remoteObjectToProtocolValue(key.value) : undefined, + value: remoteObjectToProtocolValue(value.value), + }); + } + } + if (this.overflow) { + break; + } + } +} + +function previewRegExp() { + this.extra.regexpString = this.obj.description; +} + +function previewDate() { + const dateTime = Date.parse(this.obj.description); + if (!Number.isNaN(dateTime)) { + this.extra.dateTime = dateTime; + } +} + +function previewError() { + this.addProperty({ name: "name", value: this.obj.className }, /* force */ true); +} + +const ErrorProperties = [ + "message", + "stack", + previewError, +]; + +function previewFunction(allProperties) { + const nameProperty = allProperties.result.find(prop => prop.name == "name"); + if (nameProperty) { + this.extra.functionName = nameProperty.value.value; + } + + const locationProperty = allProperties.internalProperties.find( + prop => prop.name == "[[FunctionLocation]]" + ); + if (locationProperty) { + this.extra.functionLocation = createProtocolLocation(locationProperty.value.value); + } +} + +const CustomPreviewers = { + Array: ["length"], + Int8Array: [previewTypedArray], + Uint8Array: [previewTypedArray], + Uint8ClampedArray: [previewTypedArray], + Int16Array: [previewTypedArray], + Uint16Array: [previewTypedArray], + Int32Array: [previewTypedArray], + Uint32Array: [previewTypedArray], + Float32Array: [previewTypedArray], + Float64Array: [previewTypedArray], + BigInt64Array: [previewTypedArray], + BigUint64Array: [previewTypedArray], + Map: [previewSetMap], + WeakMap: [previewSetMap], + Set: [previewSetMap], + WeakSet: [previewSetMap], + RegExp: [previewRegExp], + Date: [previewDate], + Error: ErrorProperties, + EvalError: ErrorProperties, + RangeError: ErrorProperties, + ReferenceError: ErrorProperties, + SyntaxError: ErrorProperties, + TypeError: ErrorProperties, + URIError: ErrorProperties, + Function: [previewFunction], +}; + +function createProtocolPropertyDescriptor(desc) { + const { name, value, writable, get, set, configurable, enumerable, symbol } = desc; + + const rv = value ? remoteObjectToProtocolValue(value) : {}; + rv.name = name; + + let flags = 0; + if (writable) { + flags |= 1; + } + if (configurable) { + flags |= 2; + } + if (enumerable) { + flags |= 4; + } + if (flags != 7) { + rv.flags = flags; + } + + if (get && get.objectId) { + rv.get = remoteObjectToProtocolId(get); + } + if (set && set.objectId) { + rv.set = remoteObjectToProtocolId(set); + } + + if (symbol) { + rv.isSymbol = true; + } + + return rv; +} + +function createProtocolLocation(location) { + if (!location) { + return undefined; + } + const { scriptId, lineNumber, columnNumber } = location; + return [{ + sourceId: scriptId, + // CDP line numbers are 0-indexed, while RRP line numbers are 1-indexed. + line: lineNumber + 1, + column: columnNumber, + }]; +} + +function createProtocolFrame(frameId, frame) { + // CDP call frames don't provide detailed type information. + const type = frame.functionName ? "call" : "global"; + + return { + frameId, + type, + functionName: frame.functionName || undefined, + functionLocation: createProtocolLocation(frame.functionLocation), + location: createProtocolLocation(frame.location), + scopeChain: frame.scopeChain.map(scopeToProtocolId), + this: remoteObjectToProtocolValue(frame.this), + }; +} + +function createProtocolScope(scopeId) { + const scope = protocolIdToScope(scopeId); + + let type; + switch (scope.type) { + case "global": + type = "global"; + break; + case "with": + type = "with"; + break; + default: + type = scope.name ? "function" : "block"; + break; + } + + let object, bindings; + if (type == "global" || type == "with") { + object = remoteObjectToProtocolId(scope.object); + } else { + bindings = []; + + const properties = sendMessage("Runtime.getProperties", { + objectId: scope.object.objectId, + ownProperties: true, + generatePreview: false, + }).result; + for (const { name, value } of properties) { + const converted = remoteObjectToProtocolValue(value); + bindings.push({ ...converted, name }); + } + } + + return { + scopeId, + type, + object, + functionName: scope.name || undefined, + bindings, + }; +} + +module.exports = { + createProtocolObject, + createProtocolFrame, + createProtocolLocation, + createProtocolScope, +}; diff --git a/lib/internal/recordreplay/sourcemap.js b/lib/internal/recordreplay/sourcemap.js new file mode 100644 index 000000000000..475f149be26f --- /dev/null +++ b/lib/internal/recordreplay/sourcemap.js @@ -0,0 +1,232 @@ +const crypto = require("crypto"); +const fs = require("fs"); +const path = require("path"); +const { fileURLToPath, pathToFileURL } = require("url"); +const { assert, log } = require("internal/recordreplay/utils"); + +const sourceMapInfos = new Map(); + +function getSourceMapURL(sourceId) { + return sourceMapInfos.get(sourceId)?.sourceMapURL; +} + +function registerSourceMap(ev) { + if (!ev.sourceMapURL) { + return; + } + + const { url: sourceURL, scriptId } = ev; + const sourceBaseURL = sourceURL && isValidBaseURL(sourceURL) ? sourceURL : pathToFileURL(process.cwd()); + + let sourceMapURL; + try { + sourceMapURL = new URL(ev.sourceMapURL, sourceBaseURL).toString(); + } catch (err) { + log("Failed to process sourcemap url: " + err.message); + return; + } + + sourceMapInfos.set(scriptId, { sourceURL, sourceMapURL }); +} + +function processSourceMaps() { + for (const { sourceURL, sourceMapURL } of sourceMapInfos.values()) { + processSourceMap(sourceURL, sourceMapURL); + } +} + +function processSourceMap(sourceURL, sourceMapURL) { + if (!sourceURL?.startsWith("file://") || !sourceMapURL.startsWith("file://")) { + return; + } + + const sourcePath = fileURLToPath(sourceURL); + let script; + try { + script = fs.readFileSync(sourcePath, "utf8"); + } catch (err) { + log(`Failed to read source ${sourcePath}: ${err.message}`); + } + if (!script) { + return; + } + + const sourceMapPath = fileURLToPath(sourceMapURL); + let sourceMap; + try { + sourceMap = fs.readFileSync(sourceMapPath, "utf8"); + } catch (err) { + log(`Failed to read sourcemap ${sourceMapPath}: ${err.message}`); + } + if (!sourceMap) { + return; + } + + const recordingId = process.recordreplay.recordingId(); + const id = String(Math.floor(Math.random() * 10000000000)); + const name = `sourcemap-${id}.map`; + const path = writeToRecordingDirectory(name, sourceMap); + addRecordingEvent({ + kind: "sourcemapAdded", + path, + recordingId, + id, + url: sourceMapURL, + baseURL: sourceMapURL, + targetContentHash: makeAPIHash(script), + targetURLHash: makeAPIHash(sourceURL), + targetMapURLHash: makeAPIHash(sourceMapURL), + }); + + const { sources } = collectUnresolvedSourceMapResources(sourceMap, sourceMapURL); + + for (const { offset, url } of sources) { + const sourcePath = fileURLToPath(url); + let sourceContent; + try { + sourceContent = fs.readFileSync(sourcePath, "utf8"); + } catch (err) { + log(`Failed to read original source ${sourcePath}: ${err.message}`); + continue; + } + const sourceId = String(Math.floor(Math.random() * 10000000000)); + const name = `original-source-${id}-${sourceId}`; + const path = writeToRecordingDirectory(name, sourceContent); + addRecordingEvent({ + kind: "originalSourceAdded", + path, + recordingId, + parentId: id, + parentOffset: offset, + }); + } +} + +function isValidBaseURL(url) { + try { + new URL("", url); + return true; + } catch { + return false; + } +} + +function makeAPIHash(content) { + assert(typeof content === "string"); + return "sha256:" + crypto.createHash('sha256').update(content).digest('hex'); +} + +function getRecordingDirectory() { + // see matching logic in recordreplay::InitializeRecordingEvents() in the backend, + // in getRecordingDirectory() in gecko-dev and in getDirectory() in the recordings cli + const recordingDir = process.env["RECORD_REPLAY_DIRECTORY"]; + if (recordingDir) { + return recordingDir; + } + const homeDir = process.env["HOME"] || process.env["USERPROFILE"]; + if (!homeDir) { + log("NoRecordingDirectory"); + return undefined; + } + return path.join(homeDir, ".replay"); +} + +function addRecordingEvent(event) { + const recordingDir = getRecordingDirectory(); + if (!recordingDir) { + return; + } + const filepath = path.join(recordingDir, "recordings.log"); + fs.appendFileSync(filepath, JSON.stringify(event) + "\n"); +} + +function writeToRecordingDirectory(filename, contents) { + const recordingDir = getRecordingDirectory(); + if (!recordingDir) { + return; + } + try { + fs.mkdirSync(recordingDir); + } catch {} + const filepath = path.join(recordingDir, filename); + fs.writeFileSync(filepath, contents); + return filepath; +} + +function collectUnresolvedSourceMapResources(mapText, mapURL) { + let obj; + try { + obj = JSON.parse(mapText); + if (typeof obj !== "object" || !obj) { + return { + sources: [], + }; + } + } catch (err) { + log(`Exception parsing sourcemap JSON (${mapURL})`); + return { + sources: [], + }; + } + + function logError(msg) { + log(`${msg} (${mapURL}:${sourceOffset})`); + } + + const unresolvedSources = []; + let sourceOffset = 0; + + if (obj.version !== 3) { + logError("Invalid sourcemap version"); + return { + sources: [], + }; + } + + 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; + } + if(!sourceURL.startsWith("file://")) { + continue; + } + + unresolvedSources.push({ + offset, + url: sourceURL, + }); + } + } + } else { + logError("Invalid sourcemap source list"); + } + } + + return { + sources: unresolvedSources, + }; +} + +module.exports = { + registerSourceMap, + processSourceMaps, + getSourceMapURL, +}; diff --git a/lib/internal/recordreplay/utils.js b/lib/internal/recordreplay/utils.js new file mode 100644 index 000000000000..2cef613d3820 --- /dev/null +++ b/lib/internal/recordreplay/utils.js @@ -0,0 +1,13 @@ + +function assert(v) { + if (!v) { + log(`Assertion failed ${Error().stack}`); + throw new Error("Assertion failed"); + } +} + +function log(text) { + process.recordreplay.log(text); +} + +module.exports = { assert, log }; diff --git a/node.gyp b/node.gyp index d2dbce19992b..16933ed4c205 100644 --- a/node.gyp +++ b/node.gyp @@ -888,6 +888,7 @@ ], 'sources': [ + 'src/node_record_replay_driver.cc', '<@(node_sources)', # Dependency headers 'deps/v8/include/v8.h', diff --git a/src/api/callback.cc b/src/api/callback.cc index 0f458f470501..e6efa442fb7f 100644 --- a/src/api/callback.cc +++ b/src/api/callback.cc @@ -201,6 +201,8 @@ void InternalCallbackScope::Close() { // to initializes the tick callback during bootstrap. CHECK(!tick_callback.IsEmpty()); + v8::recordreplay::Assert("InternalCallbackScope::Close CallTickCallback"); + if (tick_callback->Call(context, process, 0, nullptr).IsEmpty()) { failed_ = true; } diff --git a/src/api/environment.cc b/src/api/environment.cc index 296d33f80ff2..1c835e493601 100644 --- a/src/api/environment.cc +++ b/src/api/environment.cc @@ -114,7 +114,9 @@ MaybeLocal PrepareStackTraceCallback(Local context, void* NodeArrayBufferAllocator::Allocate(size_t size) { void* ret; - COUNT_GENERIC_USAGE("NodeArrayBufferAllocator.Allocate.ZeroFilled"); + if (zero_fill_field_ || + per_process::cli_options->zero_fill_all_buffers || + v8::recordreplay::IsRecordingOrReplaying()) ret = allocator_->Allocate(size); if (ret != nullptr) [[likely]] { total_mem_usage_.fetch_add(size, std::memory_order_relaxed); @@ -1143,7 +1145,10 @@ void DefaultProcessExitHandlerInternal(Environment* env, ExitCode exit_code) { Exit(exit_code); } +extern void RecordReplayFinishRecording(); + void DefaultProcessExitHandler(Environment* env, int exit_code) { + RecordReplayFinishRecording(); DefaultProcessExitHandlerInternal(env, static_cast(exit_code)); } diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 7b7930187bd5..28bb62828ffc 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -105,6 +105,7 @@ void Emit(Environment* env, double async_id, AsyncHooks::Fields type, void AsyncWrap::EmitPromiseResolve(Environment* env, double async_id) { + v8::recordreplay::Assert("AsyncWrap::EmitPromiseResolve"); Emit(env, async_id, AsyncHooks::kPromiseResolve, env->async_hooks_promise_resolve_function()); } @@ -139,6 +140,7 @@ void AsyncWrap::EmitTraceEventBefore() { void AsyncWrap::EmitBefore(Environment* env, double async_id) { + v8::recordreplay::Assert("AsyncWrap::EmitBefore"); Emit(env, async_id, AsyncHooks::kBefore, env->async_hooks_before_function()); } @@ -161,6 +163,7 @@ void AsyncWrap::EmitTraceEventAfter(ProviderType type, double async_id) { void AsyncWrap::EmitAfter(Environment* env, double async_id) { + v8::recordreplay::Assert("AsyncWrap::EmitAfter"); // If the user's callback failed then the after() hooks will be called at the // end of _fatalException(). Emit(env, async_id, AsyncHooks::kAfter, @@ -582,6 +585,13 @@ void AsyncWrap::EmitDestroy(Environment* env, double async_id) { return; } + // This is called non-deterministically due to GC activity. For now we no-op + // it when recording/replaying, but could record/replay the set of IDs to + // destroy instead. + if (v8::recordreplay::IsRecordingOrReplaying()) { + return; + } + if (env->destroy_async_id_list()->empty()) { env->SetImmediate(&DestroyAsyncIdsCallback, CallbackFlags::kUnrefed); } @@ -644,6 +654,8 @@ void AsyncWrap::EmitAsyncInit(Environment* env, Local type, double async_id, double trigger_async_id) { + v8::recordreplay::Assert("AsyncWrap::EmitAsyncInit"); + CHECK(!object.IsEmpty()); CHECK(!type.IsEmpty()); AsyncHooks* async_hooks = env->async_hooks(); diff --git a/src/cares_wrap.cc b/src/cares_wrap.cc index 72f3d06fe075..db12a849cd47 100644 --- a/src/cares_wrap.cc +++ b/src/cares_wrap.cc @@ -58,6 +58,7 @@ namespace node { + namespace cares_wrap { using v8::Array; @@ -927,6 +928,7 @@ void ChannelWrap::Setup() { } void ChannelWrap::StartTimer() { + v8::recordreplay::Assert("ChannelWrap::StartTimer %d", *(int*)cares_channel()); if (timer_handle_ == nullptr) { timer_handle_ = new uv_timer_t(); timer_handle_->data = static_cast(this); @@ -1798,6 +1800,8 @@ static void Query(const FunctionCallbackInfo& args) { } void AfterGetAddrInfo(uv_getaddrinfo_t* req, int status, struct addrinfo* res) { + v8::recordreplay::Assert("AfterGetAddrInfo"); + auto cleanup = OnScopeLeave([&]() { uv_freeaddrinfo(res); }); BaseObjectPtr req_wrap{ static_cast(req->data)}; @@ -1884,6 +1888,8 @@ void AfterGetAddrInfo(uv_getaddrinfo_t* req, int status, struct addrinfo* res) { void AfterGetNameInfo(uv_getnameinfo_t* req, int status, + v8::recordreplay::Assert("AfterGetNameInfo"); + const char* hostname, const char* service) { BaseObjectPtr req_wrap{ diff --git a/src/connection_wrap.cc b/src/connection_wrap.cc index 8cb6b8e66cec..086685ecbc4e 100644 --- a/src/connection_wrap.cc +++ b/src/connection_wrap.cc @@ -18,7 +18,6 @@ using v8::Local; using v8::Object; using v8::Value; - template ConnectionWrap::ConnectionWrap(Environment* env, Local object, @@ -77,6 +76,8 @@ void ConnectionWrap::OnConnection(uv_stream_t* handle, template void ConnectionWrap::AfterConnect(uv_connect_t* req, int status) { + v8::recordreplay::Assert("ConnectionWrap::AfterConnect"); + BaseObjectPtr req_wrap{static_cast(req->data)}; CHECK(req_wrap); WrapType* wrap = static_cast(req->handle->data); diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index bfa695038b32..c3c9a3c29bc9 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -2347,6 +2347,11 @@ int SecureContext::TicketKeyCallback(SSL* ssl, int enc) { static const int kTicketPartSize = 16; + // Force the data we get from OpenSSL to match when replaying, as they can + // vary for an unknown reason. + v8::recordreplay::RecordReplayBytes("TicketKeyCallback name", name, kTicketPartSize); + v8::recordreplay::RecordReplayBytes("TicketKeyCallback iv", iv, kTicketPartSize); + SecureContext* sc = static_cast( SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); diff --git a/src/env.cc b/src/env.cc index df092506ab3e..b7e81f8e3b10 100644 --- a/src/env.cc +++ b/src/env.cc @@ -829,6 +829,7 @@ Environment::Environment(IsolateData* isolate_data, thread_id_(thread_id.id == static_cast(-1) ? AllocateEnvironmentThreadId().id : thread_id.id), + native_immediates_threadsafe_mutex_(/* ordered */ true) { thread_name_(thread_name) { if (!is_main_thread()) { // If this is a Worker thread, we can always safely use the parent's @@ -1398,15 +1399,21 @@ Maybe Environment::CheckUnsettledTopLevelAwait() const { } void Environment::RunAndClearInterrupts() { - while (native_immediates_interrupts_.size() > 0) { + while (true) { NativeImmediateQueue queue; { + v8::recordreplay::Assert("native_immediates_threadsafe_mutex #1"); Mutex::ScopedLock lock(native_immediates_threadsafe_mutex_); + if (native_immediates_interrupts_.size() == 0) { + break; + } queue.ConcatMove(std::move(native_immediates_interrupts_)); } DebugSealHandleScope seal_handle_scope(isolate()); while (auto head = queue.Shift()) + v8::recordreplay::Assert("Environment::RunAndClearNativeImmediates"); + head->Call(this); } } @@ -1465,8 +1472,9 @@ void Environment::RunAndClearNativeImmediates(bool only_refed) { // This is intentionally placed after the `ref_count` handling, because when // refed threadsafe immediates are created, they are not counted towards the // count in immediate_info() either. + v8::recordreplay::Assert("native_immediates_threadsafe_mutex #2"); NativeImmediateQueue threadsafe_immediates; - if (native_immediates_threadsafe_.size() > 0) { + { Mutex::ScopedLock lock(native_immediates_threadsafe_mutex_); threadsafe_immediates.ConcatMove(std::move(native_immediates_threadsafe_)); } @@ -1474,6 +1482,9 @@ void Environment::RunAndClearNativeImmediates(bool only_refed) { } void Environment::RequestInterruptFromV8() { + // V8 interrupts will not be replayed at precise positions. + v8::recordreplay::InvalidateRecording("RequestInterruptFromV8 called"); + // The Isolate may outlive the Environment, so some logic to handle the // situation in which the Environment is destroyed before the handler runs // is required. @@ -1525,6 +1536,8 @@ void Environment::RunTimers(uv_timer_t* handle) { Environment* env = Environment::from_timer_handle(handle); TRACE_EVENT0(TRACING_CATEGORY_NODE1(environment), "RunTimers"); + v8::recordreplay::Assert("Environment::RunTimers"); + if (!env->can_call_into_js()) return; @@ -1581,8 +1594,13 @@ void Environment::RunTimers(uv_timer_t* handle) { } } - void Environment::CheckImmediate(uv_check_t* handle) { + // We're near the top of the event loop, periodically create new + // checkpoints so that the recording can be processed more efficiently. + if (v8::IsMainThread()) { + v8::recordreplay::NewCheckpoint(); + } + Environment* env = Environment::from_immediate_check_handle(handle); TRACE_EVENT0(TRACING_CATEGORY_NODE1(environment), "CheckImmediate"); @@ -1595,11 +1613,15 @@ void Environment::CheckImmediate(uv_check_t* handle) { return; do { + v8::recordreplay::Assert("Environment::CheckImmediate Callback"); MakeCallback(env->isolate(), env->process_object(), env->immediate_callback_function(), 0, - nullptr, + // https://github.com/RecordReplay/backend/issues/4886 + v8::recordreplay::Assert("Environment::GetNow"); + + v8::recordreplay::RecordReplayBytes("Environment::GetNow", &now, sizeof(now)); {0, 0}).ToLocalChecked(); } while (env->immediate_info()->has_outstanding() && env->can_call_into_js()); @@ -1918,6 +1940,8 @@ void Environment::Exit(ExitCode exit_code) { } void Environment::stop_sub_worker_contexts() { + v8::recordreplay::Assert("Environment::stop_sub_worker_contexts"); + DCHECK_EQ(Isolate::GetCurrent(), isolate()); while (!sub_worker_contexts_.empty()) { diff --git a/src/env.h b/src/env.h index 61512d9494ff..4ccf38bf26d5 100644 --- a/src/env.h +++ b/src/env.h @@ -1256,6 +1256,8 @@ class Environment final : public MemoryRetainer { std::unordered_map> released_allocated_buffers_; + int record_replay_task_queue_async_id_ = 0; + v8::CpuProfiler* cpu_profiler_ = nullptr; std::vector pending_profiles_; }; diff --git a/src/handle_wrap.cc b/src/handle_wrap.cc index 70db7ddbeab5..c202587cff0c 100644 --- a/src/handle_wrap.cc +++ b/src/handle_wrap.cc @@ -36,7 +36,6 @@ using v8::Local; using v8::Object; using v8::Value; - void HandleWrap::Ref(const FunctionCallbackInfo& args) { HandleWrap* wrap; ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); @@ -70,6 +69,8 @@ void HandleWrap::Close(const FunctionCallbackInfo& args) { } void HandleWrap::Close(Local close_callback) { + v8::recordreplay::Assert("HandleWrap::Close"); + if (state_ != kInitialized) return; @@ -86,6 +87,12 @@ void HandleWrap::Close(Local close_callback) { void HandleWrap::OnGCCollect() { + // Leak the handle when recording/replaying to avoid non-deterministic + // behavior. + if (v8::recordreplay::IsRecordingOrReplaying()) { + return; + } + // When all references to a HandleWrap are lost and the object is supposed to // be destroyed, we first call Close() to clean up the underlying libuv // handle. The OnClose callback then acquires and destroys another reference @@ -133,6 +140,8 @@ HandleWrap::HandleWrap(Environment* env, void HandleWrap::OnClose(uv_handle_t* handle) { + v8::recordreplay::Assert("HandleWrap::OnClose"); + CHECK_NOT_NULL(handle->data); BaseObjectPtr wrap { static_cast(handle->data) }; wrap->Detach(); @@ -151,9 +160,12 @@ void HandleWrap::OnClose(uv_handle_t* handle) { if (!wrap->persistent().IsEmpty() && wrap->object() ->Has(env->context(), env->handle_onclose_symbol()) - .FromMaybe(false)) { + .FromMaybe(false)) { + v8::recordreplay::Assert("HandleWrap::OnClose #1"); wrap->MakeCallback(env->handle_onclose_symbol(), 0, nullptr); } + + v8::recordreplay::Assert("HandleWrap::OnClose #2"); } Local HandleWrap::GetConstructorTemplate(Environment* env) { return GetConstructorTemplate(env->isolate_data()); diff --git a/src/inspector/main_thread_interface.cc b/src/inspector/main_thread_interface.cc index 0fca7483e95c..1b0f9cf655d5 100644 --- a/src/inspector/main_thread_interface.cc +++ b/src/inspector/main_thread_interface.cc @@ -206,6 +206,16 @@ MainThreadInterface::~MainThreadInterface() { void MainThreadInterface::Post(std::unique_ptr request) { CHECK_NOT_NULL(agent_); + + // When recording/replaying this is single threaded, and we don't want to + // interrupt V8 because that will invalidate the recording. + if (v8::recordreplay::IsRecordingOrReplaying()) { + CHECK(v8::IsMainThread()); + requests_.push_back(std::move(request)); + DispatchMessages(); + return; + } + Mutex::ScopedLock scoped_lock(requests_lock_); bool needs_notify = requests_.empty(); requests_.push_back(std::move(request)); diff --git a/src/inspector/node_string.cc b/src/inspector/node_string.cc index e2148e954217..3feea5535f61 100644 --- a/src/inspector/node_string.cc +++ b/src/inspector/node_string.cc @@ -74,6 +74,16 @@ String StringUtil::fromUTF16(const uint16_t* data, size_t length) { return String(buffer.out(), utf8_length); } + // Workaround problems using istringstream when replaying. This needs to get fixed. + /* + */ + std::string str(buffer, length); + if (!strcmp(str.c_str(), "0")) { + *ok = true; + return 0; + } + double d = strtod(str.c_str(), nullptr); + *ok = d != 0; String StringUtil::fromUTF8(const uint8_t* data, size_t length) { return std::string(reinterpret_cast(data), length); } diff --git a/src/inspector/worker_inspector.cc b/src/inspector/worker_inspector.cc index 4b6ac1021be8..a1b0ab420c7a 100644 --- a/src/inspector/worker_inspector.cc +++ b/src/inspector/worker_inspector.cc @@ -70,12 +70,22 @@ ParentInspectorHandle::ParentInspectorHandle( network_resource_manager_(network_resource_manager) {} ParentInspectorHandle::~ParentInspectorHandle() { + // Worker inspection is NYI when recording/replaying. + if (v8::recordreplay::IsRecordingOrReplaying()) { + return; + } + parent_thread_->Post( std::unique_ptr(new WorkerFinishedRequest(id_))); } void ParentInspectorHandle::WorkerStarted( std::shared_ptr worker_thread, bool waiting) { + // Worker inspection is NYI when recording/replaying. + if (v8::recordreplay::IsRecordingOrReplaying()) { + return; + } + std::unique_ptr request( new WorkerStartedRequest(id_, url_, worker_thread, waiting, name_)); parent_thread_->Post(std::move(request)); diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 5e1d9149dc75..f8094a50cf0b 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -389,6 +389,7 @@ class ChannelImpl final : public v8_inspector::V8Inspector::Channel, } void sendMessageToFrontend(const StringView& message) { + std::string raw_message = protocol::StringUtil::StringViewToUtf8(message); if (per_process::enabled_debug_list.enabled( DebugCategory::INSPECTOR_SERVER)) { std::string raw_message = protocol::StringUtil::StringViewToUtf8(message); diff --git a/src/node.cc b/src/node.cc index 4ba019ddca05..ba658f7406fe 100644 --- a/src/node.cc +++ b/src/node.cc @@ -46,6 +46,7 @@ #include "node_snapshot_builder.h" #include "node_v8_platform-inl.h" #include "node_version.h" +#include "uv.h" #if HAVE_OPENSSL #include "ncrypto.h" @@ -282,6 +283,10 @@ std::optional CallbackInfoFromArray( MaybeLocal StartExecution(Environment* env, StartExecutionCallbackWithModule cb) { + if (v8::IsMainThread()) { + v8::recordreplay::NewCheckpoint(); + } + InternalCallbackScope callback_scope( env, Object::New(env->isolate()), @@ -1051,6 +1056,202 @@ static ExitCode InitializeNodeWithArgsInternal( // We should set node_is_initialized here instead of in node::Start, // otherwise embedders using node::Init to initialize everything will not be // able to set it and native addons will not load for them. +static void (*gRecordReplayAttach)(const char* buildId); +static void (*gRecordReplayRecordCommandLineArguments)(int*, char***); +static void (*gRecordReplaySaveRecording)(const char* dir); +static void (*gRecordReplayRememberRecording)(); +static void (*gRecordReplayAddMetadata)(const char* metadata); +static void (*gRecordReplayFinishRecording)(); +static void (*gBeginCallbackRegion)(); +static void (*gEndCallbackRegion)(); +static const char* (*gRecordReplayGetRecordingId)(); +static char* (*gGetUnusableRecordingReason)(); +static void* (*gJSONCreateString)(const char*); +static void* (*gJSONCreateArray)(size_t, void**); +static void* (*gJSONCreateObject)(size_t, const char**, void**); +static char* (*gJSONToString)(void*); +static void (*gJSONFree)(void*); + +namespace recordreplay { + +static bool gRecordingFinished; + +bool IsRecordingFinished() { + return gRecordingFinished; +} + +void BeginCallbackRegion() { + if (v8::recordreplay::IsRecordingOrReplaying()) { + gBeginCallbackRegion(); + } +} + +void EndCallbackRegion() { + if (v8::recordreplay::IsRecordingOrReplaying()) { + gEndCallbackRegion(); + } +} + +} // namespace recordreplay + +void RecordReplayFinishRecording() { + if (gRecordReplayFinishRecording) { + gRecordReplayFinishRecording(); + recordreplay::gRecordingFinished = true; + + // If specified via the environment, append the recording ID to a file. + const char* env = getenv("RECORD_REPLAY_RECORDING_ID_FILE"); + if (env) { + // Don't add the recording ID when the recording is unusable. + char* reason = gGetUnusableRecordingReason(); + if (reason) { + free(reason); + return; + } + InitializeRecordReplay(&argc, &argv); + + + FILE* file = fopen(env, "a"); + if (file) { + const char* recordingId = gRecordReplayGetRecordingId(); + fprintf(file, "%s\n", recordingId); + fclose(file); + } else { + fprintf(stderr, "Error: Could not open %s for adding recording ID", env); + } + } + } +} + +template +static inline void CastPointer(const Src src, Dst* dst) { + static_assert(sizeof(Src) == sizeof(uintptr_t), "bad size"); + static_assert(sizeof(Dst) == sizeof(uintptr_t), "bad size"); + memcpy((void*)dst, (const void*)&src, sizeof(uintptr_t)); +} + +template +static void RecordReplayLoadSymbol(void* handle, const char* name, T& function) { + void* sym = dlsym(handle, name); + if (!sym) { + fprintf(stderr, "Could not find %s in Record Replay driver.\n", name); + return; + } + + CastPointer(sym, &function); +} + +extern char gBuildId[]; +extern char gRecordReplayDriver[]; +extern int gRecordReplayDriverSize; + +static std::string GetRecordingMetadata(int argc, char** argv) { + std::vector handles; + for (int i = 1; i < argc; i++) { + handles.push_back(gJSONCreateString(argv[i])); + } + void* array = gJSONCreateArray(handles.size(), &handles[0]); + const char* property = "argv"; + + void* object = gJSONCreateObject(1, &property, &array); + char* objectStr = gJSONToString(object); + std::string rv = objectStr; + + free(objectStr); + gJSONFree(object); + gJSONFree(array); + for (void* handle : handles) { + gJSONFree(handle); + } + + return rv; +} + +static void* OpenDriverHandle() { + const char* driver = getenv("RECORD_REPLAY_DRIVER"); + bool temporaryDriver = false; + + if (!driver) { + const char* tmpdir = getenv("TMPDIR"); + if (!tmpdir) { + tmpdir = "/tmp"; + } + + char filename[1024]; + snprintf(filename, sizeof(filename), "%s/recordreplay.so-XXXXXX", tmpdir); + int fd = mkstemp(filename); + if (fd < 0) { + fprintf(stderr, "mkstemp failed, can't create driver.\n"); + return nullptr; + } + + int nbytes = write(fd, gRecordReplayDriver, gRecordReplayDriverSize); + if (nbytes != gRecordReplayDriverSize) { + fprintf(stderr, "write to driver temporary file failed, can't create driver.\n"); + return nullptr; + } + + temporaryDriver = true; + driver = strdup(filename); + close(fd); + } + + void* handle = dlopen(driver, RTLD_LAZY); + + if (temporaryDriver) { + unlink(driver); + } + + return handle; +} + +static void InitializeRecordReplay(int* pargc, char*** pargv) { + if (getenv("RECORD_REPLAY_DONT_RECORD")) { + return; + } + + void* handle = OpenDriverHandle(); + + if (!handle) { + fprintf(stderr, "Loading Record Replay driver failed (%s).\n", dlerror()); + return; + } + + RecordReplayLoadSymbol(handle, "RecordReplayAttach", gRecordReplayAttach); + RecordReplayLoadSymbol(handle, "RecordReplayRecordCommandLineArguments", + gRecordReplayRecordCommandLineArguments); + RecordReplayLoadSymbol(handle, "RecordReplaySaveRecording", gRecordReplaySaveRecording); + RecordReplayLoadSymbol(handle, "RecordReplayRememberRecording", gRecordReplayRememberRecording); + RecordReplayLoadSymbol(handle, "RecordReplayAddMetadata", gRecordReplayAddMetadata); + RecordReplayLoadSymbol(handle, "RecordReplayFinishRecording", gRecordReplayFinishRecording); + RecordReplayLoadSymbol(handle, "RecordReplayGetRecordingId", gRecordReplayGetRecordingId); + RecordReplayLoadSymbol(handle, "RecordReplayGetUnusableRecordingReason", gGetUnusableRecordingReason); + RecordReplayLoadSymbol(handle, "RecordReplayBeginCallbackRegion", gBeginCallbackRegion); + RecordReplayLoadSymbol(handle, "RecordReplayEndCallbackRegion", gEndCallbackRegion); + RecordReplayLoadSymbol(handle, "RecordReplayJSONCreateString", gJSONCreateString); + RecordReplayLoadSymbol(handle, "RecordReplayJSONCreateArray", gJSONCreateArray); + RecordReplayLoadSymbol(handle, "RecordReplayJSONCreateObject", gJSONCreateObject); + RecordReplayLoadSymbol(handle, "RecordReplayJSONToString", gJSONToString); + RecordReplayLoadSymbol(handle, "RecordReplayJSONFree", gJSONFree); + + if (gRecordReplayAttach && gRecordReplayFinishRecording) { + gRecordReplayAttach(gBuildId); + gRecordReplayRecordCommandLineArguments(pargc, pargv); + v8::recordreplay::SetRecordingOrReplaying(handle); + + if (gRecordReplaySaveRecording) { + gRecordReplaySaveRecording(nullptr); + } + + gRecordReplayRememberRecording(); + + if (gRecordReplayAddMetadata) { + std::string metadata = GetRecordingMetadata(*pargc, *pargv); + gRecordReplayAddMetadata(metadata.c_str()); + } + } +} + node_is_initialized = true; return ExitCode::kNoFailure; } @@ -1141,12 +1342,19 @@ InitializeOncePerProcessInternal(const std::vector& args, printf("%s\n", NODE_VERSION); result->exit_code_ = ExitCode::kNoFailure; result->early_return_ = true; - return result; - } + return result; + } + + if (per_process::cli_options->print_build_id) { + printf("%s\n", gBuildId); + result.exit_code = 0; + result.early_return = true; + return result; + } - if (per_process::cli_options->print_bash_completion) { - std::string completion = options_parser::GetBashCompletion(); - printf("%s\n", completion.c_str()); + if (per_process::cli_options->print_bash_completion) { + std::string completion = options_parser::GetBashCompletion(); + printf("%s\n", completion.c_str()); result->exit_code_ = ExitCode::kNoFailure; result->early_return_ = true; return result; diff --git a/src/node.h b/src/node.h index 485037c72f31..e0863f389c16 100644 --- a/src/node.h +++ b/src/node.h @@ -1677,6 +1677,20 @@ NODE_EXTERN struct ssl_ctx_st* GetSSLCtx(v8::Local context, } // namespace crypto +// Record/replay stuff. +namespace recordreplay { + +bool IsRecordingFinished(); +void BeginCallbackRegion(); +void EndCallbackRegion(); + +struct AutoCallbackRegion { + AutoCallbackRegion() { BeginCallbackRegion(); } + ~AutoCallbackRegion() { EndCallbackRegion(); } +}; + +} // namespace recordreplay + } // namespace node #endif // SRC_NODE_H_ diff --git a/src/node_api.cc b/src/node_api.cc index d80569d9e92e..84a236c80bf8 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -689,9 +689,29 @@ static void napi_module_register_cb(v8::Local exports, exports, module, context, - static_cast(priv)->nm_register_func); + v8::recordreplay::IsReplaying() + ? (napi_addon_register_func)0x1 + : static_cast(priv)->nm_register_func); } +namespace node { + +void RecordReplayAddonContextRegister(addon_context_register_func* pfunc) { + if (!v8::recordreplay::IsRecordingOrReplaying()) { + return; + } + + if (v8::recordreplay::RecordReplayValue("RecordReplayAddonContextRegisterFunc", + *pfunc == napi_module_register_cb)) { + *pfunc = napi_module_register_cb; + return; + } + + v8::recordreplay::InvalidateRecording("Binary module unknown addon_context_register_func"); +} + +} // namespace node + template static void node_api_context_register_func(v8::Local exports, v8::Local module, @@ -769,7 +789,21 @@ void napi_module_register_by_symbol(v8::Local exports, napi_env env = node_napi_env__::New(context, module_filename, module_api_version); - napi_value _exports = nullptr; + v8::recordreplay::RegisterPointer(env); + v8::recordreplay::RegisterPointer(v8impl::JsValueFromV8LocalValue(exports)); + + if (v8::recordreplay::IsReplaying()) { + node::recordreplay::AutoCallbackRegion region; + int id = v8::recordreplay::RecordReplayValue("napi_module_register_by_symbol", 0); + _exports = (napi_value) v8::recordreplay::IdPointer(id); + } else { + { + node::recordreplay::AutoCallbackRegion region; + _exports = init(env, v8impl::JsValueFromV8LocalValue(exports)); + } + int id = v8::recordreplay::PointerId(_exports); + v8::recordreplay::RecordReplayValue("napi_module_register_by_symbol", id); + } env->CallIntoModule([&](napi_env env) { _exports = init(env, v8impl::JsValueFromV8LocalValue(exports)); }); diff --git a/src/node_binding.cc b/src/node_binding.cc index 6ba22f5519b4..05e428544829 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -290,6 +290,8 @@ extern "C" void node_module_register(void* m) { } } +extern void RecordReplayAddonContextRegister(addon_context_register_func* pfunc); + namespace binding { static struct global_handle_map_t { @@ -349,6 +351,7 @@ DLib::DLib(const char* filename, int flags) #ifdef __POSIX__ bool DLib::Open() { handle_ = dlopen(filename_.c_str(), flags_); + v8::recordreplay::Assert("DLib::Open %s %d", filename_.c_str(), !!handle_); if (handle_ != nullptr) return true; errmsg_ = dlerror(); return false; @@ -480,6 +483,8 @@ void DLOpen(const FunctionCallbackInfo& args) { const bool is_opened = dlib->Open(); + v8::recordreplay::Assert("DLOpen Callback #1 %d", is_opened); + // Objects containing v14 or later modules will have registered themselves // on the pending list. Activate all of them now. At present, only one // module per object is supported. @@ -497,6 +502,18 @@ void DLOpen(const FunctionCallbackInfo& args) { return false; } + // When replaying binary modules do not run, so mp won't be set. + // Reconstruct mp's contents. + if (v8::recordreplay::IsRecordingOrReplaying()) { + bool hasModule = v8::recordreplay::RecordReplayValue("DLOpen HasModule", !!mp); + if (hasModule) { + if (v8::recordreplay::IsReplaying()) { + mp = new node_module; + } + v8::recordreplay::RecordReplayBytes("DLOpen ModuleContents", mp, sizeof(node_module)); + } + } + if (mp != nullptr) { if (mp->nm_context_register_func == nullptr) { if (env->force_context_aware()) { @@ -562,8 +579,11 @@ void DLOpen(const FunctionCallbackInfo& args) { // Do not keep the lock while running userland addon loading code. Mutex::ScopedUnlock unlock(lock); if (mp->nm_context_register_func != nullptr) { - mp->nm_context_register_func(exports, module, context, mp->nm_priv); + addon_context_register_func func = mp->nm_context_register_func; + RecordReplayAddonContextRegister(&func); + func(exports, module, context, mp->nm_priv); } else if (mp->nm_register_func != nullptr) { + v8::recordreplay::InvalidateRecording("Binary module nm_register_func NYI"); mp->nm_register_func(exports, module, mp->nm_priv); } else { dlib->Close(); diff --git a/src/node_buffer.cc b/src/node_buffer.cc index 0577c302ca4a..c48e6aaa8d03 100644 --- a/src/node_buffer.cc +++ b/src/node_buffer.cc @@ -198,6 +198,13 @@ void CallbackInfo::OnBackingStoreFree() { // be gone at this point, so don’t attempt to call SetImmediateThreadsafe(). if (callback_ == nullptr) return; + // When recording/replaying the data is freed at a non-deterministic point, + // and we don't yet support posting immediates in such a case. + if (v8::recordreplay::IsRecordingOrReplaying()) { + self.release(); + return; + } + env_->SetImmediateThreadsafe([self = std::move(self)](Environment* env) { CHECK_EQ(self->env_, env); // Consistency check. diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 6eca24fdaa40..ddda49c4da3c 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -234,7 +234,11 @@ std::unordered_map> const BuiltinSource* BuiltinLoader::AddExternalizedBuiltin( const char* id, const char* filename) { StaticExternalTwoByteResource* resource; - { + + // Avoid using code cache when recording/replaying, as generated bytecode + // depends on the running thread and we might end up running code compiled + // by the main thread on a worker thread. + if (!v8::recordreplay::IsRecordingOrReplaying()) { Mutex::ScopedLock lock(externalized_builtins_mutex); auto it = externalized_builtin_sources.find(id); if (it == externalized_builtin_sources.end()) { diff --git a/src/node_dir.cc b/src/node_dir.cc index c9173d404c79..6f1b686a82ac 100644 --- a/src/node_dir.cc +++ b/src/node_dir.cc @@ -123,6 +123,13 @@ void DirHandle::New(const FunctionCallbackInfo& args) { DirHandle::~DirHandle() { CHECK(!closing_); // We should not be deleting while explicitly closing! + + // When recording/replaying we can't close directories at non-deterministic + // points. Let the directory leak. + if (v8::recordreplay::IsRecordingOrReplaying()) { + return; + } + GCClose(); // Close synchronously and emit warning CHECK(closed_); // We have to be closed at the point } diff --git a/src/node_errors.cc b/src/node_errors.cc index 63db97f6a56d..6d47a4c38fc2 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -396,6 +396,15 @@ void AppendExceptionLine(Environment* env, void Assert(const AssertionInfo& info) { std::string name = GetHumanReadableProcessName(); + if (v8::recordreplay::IsRecordingOrReplaying()) { + v8::recordreplay::Print("%s: %s:%s%s Assertion `%s' failed.\n", + name.c_str(), + info.file_line, + info.function, + *info.function ? ":" : "", + info.message); + } + fprintf(stderr, "\n" " # %s: %s at %s\n" @@ -1266,6 +1275,8 @@ void DecorateErrorStack(Environment* env, env->context(), env->decorated_private_symbol(), True(env->isolate())); } +extern "C" void V8RecordReplayOnErrorEvent(Local message); + void TriggerUncaughtException(Isolate* isolate, Local error, Local message, @@ -1273,6 +1284,8 @@ void TriggerUncaughtException(Isolate* isolate, CHECK(!error.IsEmpty()); HandleScope scope(isolate); + v8::recordreplay::Assert("TriggerUncaughtException"); + if (message.IsEmpty()) message = Exception::CreateMessage(isolate, error); CHECK(isolate->InContext()); @@ -1366,6 +1379,8 @@ void TriggerUncaughtException(Isolate* isolate, return; } + V8RecordReplayOnErrorEvent(message); + // Now we are certain that the exception is fatal. ReportFatalException(env, error, message, EnhanceFatalException::kEnhance); RunAtExit(env); diff --git a/src/node_errors.h b/src/node_errors.h index ed8e7eed9e3d..ccb21dafd13f 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -169,6 +169,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); template \ inline void THROW_##code( \ v8::Isolate* isolate, std::string_view format, Args&&... args) { \ + v8::recordreplay::Assert("ThrowException %s", format); \ isolate->ThrowException( \ code(isolate, format, std::forward(args)...)); \ } \ @@ -249,6 +250,7 @@ ERRORS_WITH_CODE(V) V(ERR_PROTO_ACCESS, \ "Accessing Object.prototype.__proto__ has been " \ "disallowed with --disable-proto=throw") + v8::recordreplay::Assert("ThrowException %s", message); \ #define V(code, message) \ inline v8::Local code(v8::Isolate* isolate) { \ diff --git a/src/node_file.cc b/src/node_file.cc index d93f213202ec..c5a956f0246c 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -722,6 +722,7 @@ int FileHandle::DoShutdown(ShutdownWrap* req_wrap) { } void FSReqCallback::Reject(Local reject) { + v8::recordreplay::Assert("FSReqCallback::Reject"); MakeCallback(env()->oncomplete_string(), 1, &reject); } @@ -734,6 +735,7 @@ void FSReqCallback::ResolveStatFs(const uv_statfs_t* stat) { } void FSReqCallback::Resolve(Local value) { + v8::recordreplay::Assert("FSReqCallback::Resolve"); Local argv[2]{Null(env()->isolate()), value}; MakeCallback(env()->oncomplete_string(), value->IsUndefined() ? 1 : arraysize(argv), @@ -2518,6 +2520,11 @@ static void WriteBuffer(const FunctionCallbackInfo& args) { char* buf = buffer_data + off; uv_buf_t uvbuf = uv_buf_init(buf, len); + // https://github.com/RecordReplay/backend/issues/4792 + v8::recordreplay::AssertScriptedCaller(args.GetIsolate(), "fs::WriteBuffer"); + v8::recordreplay::Assert("fs::WriteBuffer %d %zu %zu %zu", + fd, off, len, (size_t)pos); + FSReqBase* req_wrap_async = GetReqWrap(args, 5); if (req_wrap_async != nullptr) { // write(fd, buffer, off, len, pos, req) FS_ASYNC_TRACE_BEGIN0(UV_FS_WRITE, req_wrap_async) diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index d99b61780fad..b097985944d2 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -48,6 +48,7 @@ namespace node { + namespace http_parser { // NOLINT(build/namespaces) using v8::Array; @@ -419,6 +420,8 @@ class Parser : public AsyncWrap, public StreamListener { headers_completed_ = true; header_nread_ = 0; + v8::recordreplay::Assert("Parser::on_headers_complete"); + // Arguments for the on-headers-complete javascript callback. This // list needs to be kept in sync with the actual argument list for // `parserOnHeadersComplete` in lib/_http_common.js. diff --git a/src/node_main_instance.cc b/src/node_main_instance.cc index 6f674df3ed0d..850df05a7e7b 100644 --- a/src/node_main_instance.cc +++ b/src/node_main_instance.cc @@ -85,6 +85,8 @@ NodeMainInstance::~NodeMainInstance() { platform_->DisposeIsolate(isolate_); } +extern void RecordReplayFinishRecording(); + ExitCode NodeMainInstance::Run() { Locker locker(isolate_); Isolate::Scope isolate_scope(isolate_); @@ -107,6 +109,7 @@ void NodeMainInstance::Run(ExitCode* exit_code, Environment* env) { } *exit_code = + RecordReplayFinishRecording(); SpinEventLoopInternal(env).FromMaybe(ExitCode::kGenericUserError); } diff --git a/src/node_messaging.cc b/src/node_messaging.cc index f00ab803fef0..40738f39f762 100644 --- a/src/node_messaging.cc +++ b/src/node_messaging.cc @@ -624,7 +624,7 @@ void Message::MemoryInfo(MemoryTracker* tracker) const { } MessagePortData::MessagePortData(MessagePort* owner) - : owner_(owner) { + : mutex_(/* ordered */ true), owner_(owner) { } MessagePortData::~MessagePortData() { diff --git a/src/node_mutex.h b/src/node_mutex.h index 69e64ba27c31..29401083e3ea 100644 --- a/src/node_mutex.h +++ b/src/node_mutex.h @@ -68,7 +68,7 @@ class ExclusiveAccess { template class MutexBase { public: - inline MutexBase(); + inline MutexBase(bool ordered = false); inline ~MutexBase(); inline void Lock(); inline void Unlock(); @@ -187,6 +187,10 @@ struct LibuvMutexTraits { uv_mutex_unlock(mutex); } + static inline void mutex_mark_ordered(MutexT* mutex) { + uv_mutex_mark_ordered(mutex); + } + static inline void mutex_rdlock(MutexT* mutex) { uv_mutex_lock(mutex); } @@ -215,6 +219,11 @@ struct LibuvRwlockTraits { uv_rwlock_wrunlock(mutex); } + static inline void mutex_mark_ordered(MutexT* mutex) { + // Ordered rw locks are not supported. + CHECK(0); + } + static inline void mutex_rdlock(MutexT* mutex) { uv_rwlock_rdlock(mutex); } @@ -250,8 +259,11 @@ void ConditionVariableBase::Wait(const ScopedLock& scoped_lock) { } template -MutexBase::MutexBase() { +MutexBase::MutexBase(bool ordered) { CHECK_EQ(0, Traits::mutex_init(&mutex_)); + if (ordered) { + Traits::mutex_mark_ordered(&mutex_); + } } template diff --git a/src/node_options.cc b/src/node_options.cc index 58553081158c..f4ec9ddb2f8a 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -1398,6 +1398,9 @@ PerProcessOptionsParser::PerProcessOptionsParser( AddOption( "--version", "print Node.js version", &PerProcessOptions::print_version); AddAlias("-v", "--version"); + AddOption("--build-id", + "print record/replay build ID", + &PerProcessOptions::print_build_id); AddOption("--v8-options", "print V8 command line options", &PerProcessOptions::print_v8_help); diff --git a/src/node_options.h b/src/node_options.h index ab74dd31daab..c1cf8314ac9d 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -348,6 +348,7 @@ class PerProcessOptions : public Options { bool print_help = false; bool print_v8_help = false; bool print_version = false; + bool print_build_id = false; std::string experimental_sea_config; std::string run; diff --git a/src/node_os.cc b/src/node_os.cc index 4c7ebde4644f..9b71169a56d8 100644 --- a/src/node_os.cc +++ b/src/node_os.cc @@ -59,8 +59,24 @@ using v8::Object; using v8::String; using v8::Value; +// When recording/replaying, sometimes when side effects aren't allowed we want +// to avoid interacting with the system in potentially new ways, as this can cause +// the associated command to e.g. get an object's contents to fail. In this case +// we return an "unavailable" string. +static bool MaybeMarkUnavailable(const FunctionCallbackInfo& args) { + if (!v8::recordreplay::AllowSideEffects()) { + Environment* env = Environment::GetCurrent(args); + args.GetReturnValue().Set( + String::NewFromUtf8(env->isolate(), "unavailable").ToLocalChecked()); + return true; + } + return false; +} static void GetHostname(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Environment* env = Environment::GetCurrent(args); char buf[UV_MAXHOSTNAMESIZE]; size_t size = sizeof(buf); @@ -80,6 +96,9 @@ static void GetHostname(const FunctionCallbackInfo& args) { } static void GetOSInformation(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Environment* env = Environment::GetCurrent(args); uv_utsname_t info; int err = uv_os_uname(&info); @@ -107,6 +126,9 @@ static void GetOSInformation(const FunctionCallbackInfo& args) { } static void GetCPUInfo(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Isolate* isolate = args.GetIsolate(); uv_cpu_info_t* cpu_infos; @@ -141,6 +163,15 @@ static void GetCPUInfo(const FunctionCallbackInfo& args) { uv_free_cpu_info(cpu_infos, count); args.GetReturnValue().Set(Array::New(isolate, result.data(), result.size())); } + if (MaybeMarkUnavailable(args)) { + return; + } + if (MaybeMarkUnavailable(args)) { + return; + } + if (MaybeMarkUnavailable(args)) { + return; + } static void GetFreeMemory(const FunctionCallbackInfo& args) { @@ -183,6 +214,9 @@ static void GetUptime(const FunctionCallbackInfo& args) { static void GetLoadAvg(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } CHECK(args[0]->IsFloat64Array()); Local array = args[0].As(); CHECK_EQ(array->Length(), 3); @@ -193,6 +227,9 @@ static void GetLoadAvg(const FunctionCallbackInfo& args) { static void GetInterfaceAddresses(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Environment* env = Environment::GetCurrent(args); Isolate* isolate = env->isolate(); uv_interface_address_t* interfaces; @@ -275,6 +312,9 @@ static void GetInterfaceAddresses(const FunctionCallbackInfo& args) { static void GetHomeDirectory(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Environment* env = Environment::GetCurrent(args); char buf[PATH_MAX]; @@ -297,6 +337,9 @@ static void GetHomeDirectory(const FunctionCallbackInfo& args) { static void GetUserInfo(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Environment* env = Environment::GetCurrent(args); uv_passwd_t pwd; enum encoding encoding; @@ -391,6 +434,9 @@ static void SetPriority(const FunctionCallbackInfo& args) { static void GetPriority(const FunctionCallbackInfo& args) { + if (MaybeMarkUnavailable(args)) { + return; + } Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 2); diff --git a/src/node_process_methods.cc b/src/node_process_methods.cc index 1c695be2d5fa..1a2fda213381 100644 --- a/src/node_process_methods.cc +++ b/src/node_process_methods.cc @@ -14,6 +14,7 @@ #include "uv.h" #include "v8-fast-api-calls.h" #include "v8.h" +#include "v8-inspector.h" #include @@ -37,6 +38,23 @@ typedef int mode_t; #include #endif +static const char* AnnotationHookJSName = "recordreplay.annotationHook"; + +namespace v8 { + +extern void FunctionCallbackIsRecordingOrReplaying(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplayOnConsoleAPI(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplaySetCommandCallback(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplaySetClearPauseDataCallback(const FunctionCallbackInfo& callArgs); +extern void FunctionCallbackRecordReplayIgnoreScript(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplayAssert(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplayGetCurrentError(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplayGetRecordingId(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplayCurrentExecutionPoint(const FunctionCallbackInfo& args); +extern void FunctionCallbackRecordReplayElapsedTimeMs(const FunctionCallbackInfo& args); + +} + namespace node { using v8::Array; @@ -246,6 +264,9 @@ static void MemoryUsage(const FunctionCallbackInfo& args) { array_buffer_allocator == nullptr ? 0 : static_cast(array_buffer_allocator->total_mem_usage()); + + // Ensure memory usage measurements are consistent when replaying. + v8::recordreplay::RecordReplayBytes("MemoryUsage", fields, 5 * sizeof(double)); } static void GetConstrainedMemory(const FunctionCallbackInfo& args) { @@ -676,6 +697,101 @@ BindingData::BindingData(Realm* realm, hrtime_buffer_.MakeWeak(); } +static void RecordReplayLog(const FunctionCallbackInfo& args) { + CHECK(args.Length() == 1 && args[0]->IsString() && + "must be called with a single string"); + Utf8Value text(args.GetIsolate(), args[0]); + v8::recordreplay::Print("%s", text.ToString().c_str()); +} + +// Function to invoke on CDP responses and events. +static v8::Eternal* gCDPMessageCallback; + +static void RecordReplaySetCDPMessageCallback(const FunctionCallbackInfo& args) { + CHECK(!gCDPMessageCallback); + Isolate* isolate = args.GetIsolate(); + CHECK(args[0]->IsFunction()); + Local callback = args[0].As(); + gCDPMessageCallback = new v8::Eternal(isolate, callback); +} + +static std::unique_ptr gRecordReplayInspectorSession; + +class RecordReplaySessionDelegate : public inspector::InspectorSessionDelegate { + public: + void SendMessageToFrontend(const v8_inspector::StringView& message) override { + CHECK(v8::IsMainThread()); + + if (recordreplay::IsRecordingFinished()) { + return; + } + + CHECK(gCDPMessageCallback); + CHECK(!message.is8Bit()); + + Isolate* isolate = v8::Isolate::GetCurrent(); + v8::HandleScope scope(isolate); + + Local context = isolate->GetCurrentContext(); + + Local arg = v8::String::NewFromTwoByte(isolate, message.characters16(), + NewStringType::kNormal, + message.length()).ToLocalChecked(); + Local callback = gCDPMessageCallback->Get(isolate); + v8::MaybeLocal rv = callback->Call(context, v8::Undefined(isolate), 1, &arg); + CHECK(!rv.IsEmpty()); + } +}; + +static void RecordReplaySendCDPMessage(const FunctionCallbackInfo& args) { + CHECK(args.Length() == 1 && args[0]->IsString() && + "must be called with a single string"); + Utf8Value message(args.GetIsolate(), args[0]); + + if (!gRecordReplayInspectorSession) { + Environment* env = Environment::GetCurrent(args); + inspector::Agent* agent = env->inspector_agent(); + + auto delegate = std::make_unique(); + gRecordReplayInspectorSession = agent->Connect(std::move(delegate), + /* prevent_shutdown */ false); + } + + std::string nmessage(message.ToString()); + v8_inspector::StringView messageView((const uint8_t*)nmessage.c_str(), nmessage.length()); + gRecordReplayInspectorSession->Dispatch(messageView); +} + +// Called from javascript. +// `recordreplay.annotationHook(kind, contents)` +// Since this function is called from userland JS, we avoid assertions. +// We don't want flawed uses of the API to crash the recording. +static void RecordReplayAnnotationHook( + const FunctionCallbackInfo& args) { + if (!(args.Length() >= 2 && args[0]->IsString())) { + v8::recordreplay::Print("[RuntimeError] %s called with incorrect arguments", + AnnotationHookJSName); + return; + } + + v8::Isolate* isolate = args.GetIsolate(); + v8::Local payload = v8::Object::New(isolate); + v8::Local context = isolate->GetCurrentContext(); + payload->Set(context, v8::String::NewFromUtf8(isolate, "message").ToLocalChecked(), args[1]).Check(); + + v8::Local json; + if (!v8::JSON::Stringify(context, payload).ToLocal(&json)) { + v8::recordreplay::Print( + "[RuntimeError] %s contents failed to json stringify", + AnnotationHookJSName); + return; + } + + v8::String::Utf8Value kind(args.GetIsolate(), args[0]); + v8::String::Utf8Value contents(args.GetIsolate(), json); + v8::recordreplay::OnAnnotation(*kind, *contents); +} + CFunction BindingData::fast_hrtime_(CFunction::Make(FastHrtime)); CFunction BindingData::fast_hrtime_bigint_(CFunction::Make(FastHrtimeBigInt)); @@ -731,12 +847,55 @@ void BindingData::HrtimeBigIntImpl(BindingData* receiver) { fields[0] = t; } + env->SetMethod(target, "isRecordingOrReplaying", + v8::FunctionCallbackIsRecordingOrReplaying); + env->SetMethod(target, "recordReplayLog", RecordReplayLog); + env->SetMethod(target, "recordReplayOnConsoleAPI", + v8::FunctionCallbackRecordReplayOnConsoleAPI); + env->SetMethod(target, "recordReplaySetCommandCallback", + v8::FunctionCallbackRecordReplaySetCommandCallback); + env->SetMethod(target, "recordReplaySetClearPauseDataCallback", + v8::FunctionCallbackRecordReplaySetClearPauseDataCallback); + env->SetMethod(target, "recordReplayIgnoreScript", + v8::FunctionCallbackRecordReplayIgnoreScript); + env->SetMethod(target, "recordReplayAssert", + v8::FunctionCallbackRecordReplayAssert); + env->SetMethod(target, "recordReplayGetCurrentError", + v8::FunctionCallbackRecordReplayGetCurrentError); + env->SetMethod(target, "recordReplaySetCDPMessageCallback", + RecordReplaySetCDPMessageCallback); + env->SetMethod(target, "recordReplaySendCDPMessage", + RecordReplaySendCDPMessage); + env->SetMethod(target, "recordReplayRecordingId", + v8::FunctionCallbackRecordReplayGetRecordingId); + env->SetMethod(target, "recordReplayCurrentExecutionPoint", + v8::FunctionCallbackRecordReplayCurrentExecutionPoint); + env->SetMethod(target, "recordReplayElapsedTimeMs", + v8::FunctionCallbackRecordReplayElapsedTimeMs); + env->SetMethod( + target, "recordReplayAnnotationHook", RecordReplayAnnotationHook); + void BindingData::SlowHrtimeBigInt(const FunctionCallbackInfo& args) { HrtimeBigIntImpl(FromJSObject(args.This())); } void BindingData::SlowHrtime(const FunctionCallbackInfo& args) { HrtimeImpl(FromJSObject(args.This())); + + registry->Register(v8::FunctionCallbackIsRecordingOrReplaying); + registry->Register(RecordReplayLog); + registry->Register(v8::FunctionCallbackRecordReplayOnConsoleAPI); + registry->Register(v8::FunctionCallbackRecordReplaySetCommandCallback); + registry->Register(v8::FunctionCallbackRecordReplaySetClearPauseDataCallback); + registry->Register(v8::FunctionCallbackRecordReplayIgnoreScript); + registry->Register(v8::FunctionCallbackRecordReplayAssert); + registry->Register(v8::FunctionCallbackRecordReplayGetCurrentError); + registry->Register(RecordReplaySetCDPMessageCallback); + registry->Register(RecordReplaySendCDPMessage); + registry->Register(v8::FunctionCallbackRecordReplayGetRecordingId); + registry->Register(v8::FunctionCallbackRecordReplayCurrentExecutionPoint); + registry->Register(v8::FunctionCallbackRecordReplayElapsedTimeMs); + registry->Register(RecordReplayAnnotationHook); } bool BindingData::PrepareForSerialization(Local context, diff --git a/src/node_v8.cc b/src/node_v8.cc index b49c29443a42..86f07762e6fc 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -195,7 +195,14 @@ void CachedDataVersionTag(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(result); } -void SetHeapSnapshotNearHeapLimit(const FunctionCallbackInfo& args) { +static inline double RecordReplayDouble(const char* why, double d) { + v8::recordreplay::RecordReplayBytes("UpdateHeapStatisticsBuffer", &d, sizeof(d)); + return d; +} + +#define V(index, name, _) \ + buffer[index] = RecordReplayDouble("UpdateHeapStatisticsBuffer", \ + static_cast(s.name())); CHECK(args[0]->IsUint32()); Environment* env = Environment::GetCurrent(args); uint32_t limit = args[0].As()->Value(); @@ -225,7 +232,9 @@ void UpdateHeapSpaceStatisticsBuffer(const FunctionCallbackInfo& args) { AliasedFloat64Array& buffer = data->heap_space_statistics_buffer; -#define V(index, name, _) buffer[index] = static_cast(s.name()); +#define V(index, name, _) \ + buffer[index] = RecordReplayDouble("UpdateHeapSpaceStatisticsBuffer", \ + static_cast(s.name())); HEAP_SPACE_STATISTICS_PROPERTIES(V) #undef V } @@ -236,7 +245,9 @@ void UpdateHeapCodeStatisticsBuffer(const FunctionCallbackInfo& args) { args.GetIsolate()->GetHeapCodeAndMetadataStatistics(&s); AliasedFloat64Array& buffer = data->heap_code_statistics_buffer; -#define V(index, name, _) buffer[index] = static_cast(s.name()); +#define V(index, name, _) \ + buffer[index] = RecordReplayDouble("UpdateHeapCodeStatisticsBuffer", \ + static_cast(s.name())); HEAP_CODE_STATISTICS_PROPERTIES(V) #undef V } diff --git a/src/node_worker.cc b/src/node_worker.cc index edc21e7e5561..e1b5beb5c657 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -52,6 +52,7 @@ using v8::TryCatch; using v8::Value; namespace node { + namespace worker { constexpr double kMB = 1024 * 1024; @@ -272,8 +273,12 @@ class WorkerThreadData { size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit, size_t initial_heap_limit) { - Worker* worker = static_cast(data); - // Give the current GC some extra leeway to let it finish rather than + // We can't force workers to exit at non-deterministic points when + // recording/replaying. + if (!v8::recordreplay::IsRecordingOrReplaying()) { + Worker* worker = static_cast(data); + worker->Exit(1, "ERR_WORKER_OUT_OF_MEMORY", "JS heap out of memory"); + } // crash hard. We are not going to perform further allocations anyway. constexpr size_t kExtraHeapAllowance = 16 * 1024 * 1024; size_t new_limit = current_heap_limit + kExtraHeapAllowance; @@ -452,6 +457,10 @@ bool Worker::CreateEnvMessagePort(Environment* env) { if (child_port != nullptr) env->set_message_port(child_port->object(isolate_)); + v8::recordreplay::Assert("Worker::JoinThread"); + + v8::recordreplay::Assert("Worker::JoinThread Joined"); + return child_port; } @@ -486,6 +495,7 @@ void Worker::JoinThread() { : Null(env()->isolate()).As(), }; + v8::recordreplay::Assert("Worker::JoinThread Callback"); MakeCallback(env()->onexit_string(), arraysize(args), args); } diff --git a/src/stream_base.cc b/src/stream_base.cc index 370b8f682ead..7bbc8d33c100 100644 --- a/src/stream_base.cc +++ b/src/stream_base.cc @@ -446,6 +446,8 @@ MaybeLocal StreamBase::CallJSOnreadMethod(ssize_t nread, StreamBaseJSChecks checks) { Environment* env = env_; + v8::recordreplay::Assert("StreamBase::CallJSOnreadMethod"); + DCHECK_EQ(static_cast(nread), nread); DCHECK_LE(offset, INT32_MAX); @@ -751,6 +753,8 @@ void CustomBufferJSListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) { void ReportWritesToJSStreamListener::OnStreamAfterReqFinished( StreamReq* req_wrap, int status) { + v8::recordreplay::Assert("ReportWritesToJSStreamListener::OnStreamAfterReqFinished"); + StreamBase* stream = static_cast(stream_); Environment* env = stream->stream_env(); if (!env->can_call_into_js()) return; diff --git a/src/stream_pipe.cc b/src/stream_pipe.cc index 512c3f16e441..5cb35380622f 100644 --- a/src/stream_pipe.cc +++ b/src/stream_pipe.cc @@ -156,6 +156,8 @@ void StreamPipe::ProcessData(size_t nread, void StreamPipe::WritableListener::OnStreamAfterWrite(WriteWrap* w, int status) { + v8::recordreplay::Assert("StreamPipe::WritableListener::OnStreamAfterWrite"); + StreamPipe* pipe = ContainerOf(&StreamPipe::writable_listener_, this); pipe->pending_writes_--; if (pipe->is_closed_) { diff --git a/src/tracing/agent.cc b/src/tracing/agent.cc index eddcf6c3bf91..c76dc4d0b41c 100644 --- a/src/tracing/agent.cc +++ b/src/tracing/agent.cc @@ -237,6 +237,10 @@ void TracingController::AddMetadataEvent( const uint64_t* arg_values, std::unique_ptr* convertable_values, unsigned int flags) { + // For now we don't support tracing when recording/replaying, + // due to non-deterministic behavior it can introduce. + v8::recordreplay::InvalidateRecording("Event tracing enabled"); + std::unique_ptr trace_event(new TraceObject); trace_event->Initialize( TRACE_EVENT_PHASE_METADATA, category_group_enabled, name, diff --git a/src/udp_wrap.cc b/src/udp_wrap.cc index b5e732dd6f1c..9a38d3ee6c19 100644 --- a/src/udp_wrap.cc +++ b/src/udp_wrap.cc @@ -717,6 +717,7 @@ int UDPWrap::RecvStop() { void UDPWrap::OnSendDone(ReqWrap* req, int status) { + v8::recordreplay::Assert("UDPWrap::OnSendDone"); BaseObjectPtr req_wrap{static_cast(req)}; if (req_wrap->have_callback()) { Environment* env = req_wrap->env(); diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 69f1d9f44c9c..47bfddcc4bf1 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -117,6 +117,11 @@ expected.beforePreExec = new Set([ 'NativeModule internal/net', 'NativeModule internal/dns/utils', 'NativeModule internal/modules/esm/get_format', + 'NativeModule internal/recordreplay/main', + 'NativeModule internal/recordreplay/message', + 'NativeModule internal/recordreplay/object', + 'NativeModule internal/recordreplay/preview', + 'NativeModule internal/recordreplay/utils', ]); expected.atRunTime = new Set([ From c0192dcc9c151c5509eda358556046f6bd29b55f Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Sat, 6 Jun 2026 15:59:53 -0400 Subject: [PATCH 02/40] Resolve 11 drifted conflicts by hand - node_perf.{cc,h,common.h}: re-anchor timeOrigin->performance_process_start (non-const + InitPerformance); TODO: wire InitPerformance() call site - node_credentials.cc: SafeGetenv divergence guard (refactored to linux_at_secure) - crypto_random.cc: AssertBytes on ncrypto::CSPRNG output - node_platform.cc: ordered-lock arg on TaskQueue - deps/uv/async.c: re-instrument ordered locks for C11-atomics rewrite - deps/uv/fs.c: re-instrument uv__fs_read (uv__fs_preadv removed) - openssl/err_prn.c: constant tid (OpenSSL 3.x rewrite) - README.md, node_version.h: dropped (cosmetic/obsolete) --- deps/openssl/openssl/crypto/err/err_prn.c | 4 +- deps/uv/src/unix/async.c | 24 +++++++- deps/uv/src/unix/fs.c | 68 ++++++++++++++++++++--- src/crypto/crypto_random.cc | 8 ++- src/node_credentials.cc | 12 ++-- src/node_perf.cc | 24 +++++++- src/node_perf_common.h | 8 ++- src/node_platform.cc | 21 ++++++- 8 files changed, 146 insertions(+), 23 deletions(-) diff --git a/deps/openssl/openssl/crypto/err/err_prn.c b/deps/openssl/openssl/crypto/err/err_prn.c index a6311098e3cb..c9da55154b30 100644 --- a/deps/openssl/openssl/crypto/err/err_prn.c +++ b/deps/openssl/openssl/crypto/err/err_prn.c @@ -34,7 +34,9 @@ void ERR_print_errors_cb(int (*cb)(const char *str, size_t len, void *u), data = ""; hex = ossl_buf2hexstr_sep((const unsigned char *)&tid, sizeof(tid), '\0'); - BIO_snprintf(buf, sizeof(buf), "%s:", hex == NULL ? "" : hex); + /* Replay port: emit a constant thread id; the real one varies when + * replaying, which would diverge the recording. */ + BIO_snprintf(buf, sizeof(buf), "0:"); offset = strlen(buf); ossl_err_string_int(l, func, buf + offset, sizeof(buf) - offset); offset += strlen(buf + offset); diff --git a/deps/uv/src/unix/async.c b/deps/uv/src/unix/async.c index f7995fc35197..431cf39e4995 100644 --- a/deps/uv/src/unix/async.c +++ b/deps/uv/src/unix/async.c @@ -70,6 +70,12 @@ static void uv__async_send(uv_loop_t* loop); static int uv__async_start(uv_loop_t* loop); static void uv__cpu_relax(void); +/* Replay port: ordered locks make cross-thread async wakeups deterministic. */ +extern void V8RecordReplayAssert(const char* format, ...); +extern size_t V8RecordReplayCreateOrderedLock(const char* name); +extern void V8RecordReplayOrderedLock(int lock); +extern void V8RecordReplayOrderedUnlock(int lock); + int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) { int err; @@ -81,6 +87,7 @@ int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) { uv__handle_init(loop, (uv_handle_t*)handle, UV_ASYNC); handle->async_cb = async_cb; handle->pending = 0; + handle->ordered_lock_id = V8RecordReplayCreateOrderedLock("uv_async_t"); handle->u.fd = 0; /* This will be used as a busy flag. */ uv__queue_insert_tail(&loop->async_handles, &handle->queue); @@ -97,9 +104,13 @@ int uv_async_send(uv_async_t* handle) { pending = (_Atomic int*) &handle->pending; busy = (_Atomic int*) &handle->u.fd; + V8RecordReplayOrderedLock(handle->ordered_lock_id); + /* Do a cheap read first. */ - if (atomic_load_explicit(pending, memory_order_relaxed) != 0) + if (atomic_load_explicit(pending, memory_order_relaxed) != 0) { + V8RecordReplayOrderedUnlock(handle->ordered_lock_id); return 0; + } /* Set the loop to busy. */ atomic_fetch_add(busy, 1); @@ -111,6 +122,7 @@ int uv_async_send(uv_async_t* handle) { /* Set the loop to not-busy. */ atomic_fetch_add(busy, -1); + V8RecordReplayOrderedUnlock(handle->ordered_lock_id); return 0; } @@ -129,13 +141,17 @@ static void uv__async_spin(uv_async_t* handle) { * threads after this function returns. */ atomic_store(pending, 1); + V8RecordReplayOrderedLock(handle->ordered_lock_id); + for (;;) { /* 997 is not completely chosen at random. It's a prime number, acyclic by * nature, and should therefore hopefully dampen sympathetic resonance. */ for (i = 0; i < 997; i++) { - if (atomic_load(busy) == 0) + if (atomic_load(busy) == 0) { + V8RecordReplayOrderedUnlock(handle->ordered_lock_id); return; + } /* Other thread is busy with this handle, spin until it's done. */ uv__cpu_relax(); @@ -165,6 +181,8 @@ void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) { uv_async_t* h; _Atomic int *pending; + V8RecordReplayAssert("uv__async_io"); + assert(w == &loop->async_io_watcher); #if UV__KQUEUE_EVFILT_USER @@ -191,6 +209,8 @@ void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) { uv__queue_move(&loop->async_handles, &queue); while (!uv__queue_empty(&queue)) { + V8RecordReplayAssert("uv__async_io #2"); + q = uv__queue_head(&queue); h = uv__queue_data(q, uv_async_t, queue); diff --git a/deps/uv/src/unix/fs.c b/deps/uv/src/unix/fs.c index 90cb40d98d47..f4f4280ec25f 100644 --- a/deps/uv/src/unix/fs.c +++ b/deps/uv/src/unix/fs.c @@ -46,6 +46,8 @@ #include #include +extern void V8RecordReplayAssert(const char* format, ...); + #if defined(__linux__) # include #endif @@ -138,6 +140,7 @@ extern char *mkdtemp(char *template); /* See issue #740 on AIX < 7 */ #define POST \ do { \ + V8RecordReplayAssert("POST_WORK %d", (int)req->fs_type); \ if (cb != NULL) { \ uv__req_register(loop); \ uv__work_submit(loop, \ @@ -149,6 +152,7 @@ extern char *mkdtemp(char *template); /* See issue #740 on AIX < 7 */ } \ else { \ uv__fs_work(&req->work_req); \ + V8RecordReplayAssert("POST_WORK_DONE %d %d", (int)req->fs_type, req->result); \ return req->result; \ } \ } \ @@ -361,7 +365,10 @@ static int uv__fs_mkstemp(uv_fs_t* req) { static ssize_t uv__fs_open(uv_fs_t* req) { #ifdef O_CLOEXEC - return open(req->path, req->flags | O_CLOEXEC, req->mode); + ssize_t rv = open(req->path, req->flags | O_CLOEXEC, req->mode); + V8RecordReplayAssert("uv__fs_open %s %d %d %d", + req->path, req->flags, req->mode, rv); + return rv; #else /* O_CLOEXEC */ int r; @@ -370,6 +377,9 @@ static ssize_t uv__fs_open(uv_fs_t* req) { r = open(req->path, req->flags, req->mode); + V8RecordReplayAssert("uv__fs_open %s %d %d %d", + req->path, req->flags, req->mode, r); + /* In case of failure `uv__cloexec` will leave error in `errno`, * so it is enough to just set `r` to `-1`. */ @@ -377,6 +387,7 @@ static ssize_t uv__fs_open(uv_fs_t* req) { r = uv__close(r); if (r != 0) abort(); + V8RecordReplayAssert("uv__fs_open #1"); r = -1; } @@ -520,16 +531,23 @@ static ssize_t uv__fs_read(uv_fs_t* req) { if (nbufs > iovmax) nbufs = iovmax; + // Replay port: record/replay file read parameters so reads are deterministic. + V8RecordReplayAssert("uv__fs_read start %d %u %ld", fd, nbufs, (long)off); + r = 0; if (off < 0) { - if (nbufs == 1) + if (nbufs == 1) { + V8RecordReplayAssert("uv__fs_read read %d %lu", fd, + (unsigned long)bufs->iov_len); r = read(fd, bufs->iov_base, bufs->iov_len); - else if (nbufs > 1) + } else if (nbufs > 1) r = readv(fd, bufs, nbufs); } else { - if (nbufs == 1) + if (nbufs == 1) { + V8RecordReplayAssert("uv__fs_read pread %d %lu %ld", fd, + (unsigned long)bufs->iov_len, (long)off); r = pread(fd, bufs->iov_base, bufs->iov_len, off); - else if (nbufs > 1) + } else if (nbufs > 1) r = uv__preadv(fd, bufs, nbufs, off); } @@ -880,10 +898,15 @@ static ssize_t uv__fs_sendfile_emul(uv_fs_t* req) { buflen = sizeof(buf); do - if (use_pread) + if (use_pread) { + V8RecordReplayAssert("uv__fs_sendfile_emul pread %d %lu %lu", + in_fd, buflen, offset); nread = pread(in_fd, buf, buflen, offset); - else + } else { + V8RecordReplayAssert("uv__fs_sendfile_emul read %d %lu", in_fd, + buflen); nread = read(in_fd, buf, buflen); + } while (nread == -1 && errno == EINTR); if (nread == 0) @@ -1192,6 +1215,11 @@ static ssize_t uv__fs_lutime(uv_fs_t* req) { #else errno = ENOSYS; return -1; + // https://github.com/RecordReplay/backend/issues/4792 + V8RecordReplayAssert("uv__fs_write start %d %d %d %d", + req->file, (int)req->off, (int)req->nbufs, + req->nbufs ? (int)req->bufs[0].len : -1); + #endif } @@ -1637,6 +1665,9 @@ static ssize_t uv__fs_write_all(uv_fs_t* req) { ssize_t total; ssize_t result; + // https://github.com/RecordReplay/backend/issues/4792 + V8RecordReplayAssert("uv__fs_write_all start"); + iovmax = uv__getiovmax(); nbufs = req->nbufs; bufs = req->bufs; @@ -1647,9 +1678,12 @@ static ssize_t uv__fs_write_all(uv_fs_t* req) { if (req->nbufs > iovmax) req->nbufs = iovmax; - do + do { result = uv__fs_write(req); - while (result < 0 && errno == EINTR); + + // https://github.com/RecordReplay/backend/issues/4792 + V8RecordReplayAssert("uv__fs_write_all #1 %d", (int)result); + } while (result < 0 && errno == EINTR); if (result <= 0) { if (total == 0) @@ -1672,6 +1706,9 @@ static ssize_t uv__fs_write_all(uv_fs_t* req) { req->bufs = NULL; req->nbufs = 0; + // https://github.com/RecordReplay/backend/issues/4792 + V8RecordReplayAssert("uv__fs_write_all done %d", (int)total); + return total; } @@ -1688,6 +1725,8 @@ static void uv__fs_work(struct uv__work* w) { do { errno = 0; + V8RecordReplayAssert("uv__fs_work %d", (int)req->fs_type); + #define X(type, action) \ case UV_FS_ ## type: \ r = action; \ @@ -1733,6 +1772,8 @@ static void uv__fs_work(struct uv__work* w) { default: abort(); } #undef X + + V8RecordReplayAssert("uv__fs_work #1 %d", r); } while (r == -1 && errno == EINTR && retry_on_eintr); if (r == -1) @@ -1740,6 +1781,8 @@ static void uv__fs_work(struct uv__work* w) { else req->result = r; + V8RecordReplayAssert("uv__fs_work #2 %d %d", r, req->result); + if (r == 0 && (req->fs_type == UV_FS_STAT || req->fs_type == UV_FS_FSTAT || req->fs_type == UV_FS_LSTAT)) { @@ -2002,10 +2045,15 @@ int uv_fs_open(uv_loop_t* loop, int flags, int mode, uv_fs_cb cb) { + V8RecordReplayAssert("uv_fs_open %s %d %d", path, flags, mode); + INIT(OPEN); PATH; req->flags = flags; req->mode = mode; + + V8RecordReplayAssert("uv_fs_open START_POST %d", !!cb); + if (cb != NULL) if (uv__iou_fs_open(loop, req)) return 0; @@ -2021,6 +2069,8 @@ int uv_fs_read(uv_loop_t* loop, uv_fs_t* req, uv_fs_cb cb) { INIT(READ); + V8RecordReplayAssert("uv_fs_read %d", (int)req->fs_type); + if (bufs == NULL || nbufs == 0) return UV_EINVAL; diff --git a/src/crypto/crypto_random.cc b/src/crypto/crypto_random.cc index 0318d9157b94..436a9ffab541 100644 --- a/src/crypto/crypto_random.cc +++ b/src/crypto/crypto_random.cc @@ -70,7 +70,13 @@ bool RandomBytesTraits::DeriveBits(Environment* env, ByteSource* unused, CryptoJobMode mode, CryptoErrorStore* errors) { - return ncrypto::CSPRNG(params.buffer, params.size); + bool rv = ncrypto::CSPRNG(params.buffer, params.size); + + // Replay port: record/replay the generated bytes so they are deterministic. + v8::recordreplay::AssertBytes("RandomBytesTraits::DeriveBits", + params.buffer, params.size); + + return rv; } void RandomPrimeConfig::MemoryInfo(MemoryTracker* tracker) const { diff --git a/src/node_credentials.cc b/src/node_credentials.cc index 0c7978a4843c..a3e5cd9a1d10 100644 --- a/src/node_credentials.cc +++ b/src/node_credentials.cc @@ -73,13 +73,17 @@ static bool HasOnly(int capability) { // setuid root then lookup will not be allowed. bool SafeGetenv(const char* key, std::string* text, Environment* env) { #if !defined(__CloudABI__) && !defined(_WIN32) + // Replay port: avoid interacting with the system when diverged from the + // recording. We can call getenv(), but can't figure out user IDs. + if (!v8::recordreplay::HasDivergedFromRecording()) { #if defined(__linux__) - if ((!HasOnly(CAP_NET_BIND_SERVICE) && linux_at_secure()) || - getuid() != geteuid() || getgid() != getegid()) + if ((!HasOnly(CAP_NET_BIND_SERVICE) && linux_at_secure()) || + getuid() != geteuid() || getgid() != getegid()) #else - if (linux_at_secure() || getuid() != geteuid() || getgid() != getegid()) + if (linux_at_secure() || getuid() != geteuid() || getgid() != getegid()) #endif - return false; + return false; + } #endif // Fallback to system environment which reads the real environment variable diff --git a/src/node_perf.cc b/src/node_perf.cc index e984fd4c3bf0..5b855f3a01d4 100644 --- a/src/node_perf.cc +++ b/src/node_perf.cc @@ -12,6 +12,7 @@ #include namespace node { + namespace performance { using v8::Array; @@ -36,11 +37,27 @@ using v8::Value; // Nanoseconds in a millisecond, as a float. #define NANOS_PER_MILLIS 1e6 -const uint64_t performance_process_start = PERFORMANCE_NOW(); -const double performance_process_start_timestamp = +// Replay port: in v16.14 these were file-scope `timeOrigin`/`timeOriginTimestamp` +// and InitPerformance() set them so record/replay sees consistent values instead +// of the non-deterministic static-init order. In Node 27 they were renamed to +// performance_process_start[_timestamp]. They are made non-const so +// InitPerformance() can still override them deterministically; the static +// initializer is kept as a safe default because Environment now caches these at +// construction (env.cc), unlike v16.14. +// TODO(replay-port): call InitPerformance() before the first Environment is +// constructed when recording/replaying (was implicit via static-init in v16.14). +uint64_t performance_process_start = PERFORMANCE_NOW(); +double performance_process_start_timestamp = GetCurrentTimeInMicroseconds(); uint64_t performance_v8_start; +void InitPerformance() { + // Re-capture deterministically so recording/replaying agree regardless of + // static-init order. + performance_process_start = PERFORMANCE_NOW(); + performance_process_start_timestamp = GetCurrentTimeInMicroseconds(); +} + PerformanceState::PerformanceState(Isolate* isolate, uint64_t time_origin, double time_origin_timestamp, @@ -251,6 +268,9 @@ void Notify(const FunctionCallbackInfo& args) { AliasedUint32Array& observers = env->performance_state()->observers; if (entry_type != NODE_PERFORMANCE_ENTRY_TYPE_INVALID && observers[entry_type]) { + // Performance entries can be non-deterministic and are not currently + // supported when recording/replaying. + v8::recordreplay::InvalidateRecording("Performance entries observed"); USE(env->performance_entry_callback()-> Call(env->context(), Undefined(env->isolate()), 1, &entry)); } diff --git a/src/node_perf_common.h b/src/node_perf_common.h index 01e7f35241ac..b4546b4bc831 100644 --- a/src/node_perf_common.h +++ b/src/node_perf_common.h @@ -20,10 +20,14 @@ namespace performance { // These occur before the environment is created. Cache them // here and add them to the milestones when the env is init'd. -extern const uint64_t performance_process_start; -extern const double performance_process_start_timestamp; +// Replay port: non-const so InitPerformance() can reset them deterministically. +extern uint64_t performance_process_start; +extern double performance_process_start_timestamp; extern uint64_t performance_v8_start; +// Replay port: re-capture process-start time deterministically for record/replay. +void InitPerformance(); + #define NODE_PERFORMANCE_MILESTONES(V) \ V(TIME_ORIGIN_TIMESTAMP, "timeOriginTimestamp") \ V(TIME_ORIGIN, "timeOrigin") \ diff --git a/src/node_platform.cc b/src/node_platform.cc index 197102068b74..9881215b3040 100644 --- a/src/node_platform.cc +++ b/src/node_platform.cc @@ -336,6 +336,12 @@ void PerIsolatePlatformData::PostIdleTaskImpl( UNREACHABLE(); } + if (task->IsRecordReplayNonDeterministic() && v8::recordreplay::IsRecordingOrReplaying()) { + // For now we ignore non-deterministic tasks. This prevents the GC from working. + return; + } + v8::recordreplay::Assert("PerIsolatePlatformData::PostTask"); + void PerIsolatePlatformData::PostTaskImpl(std::unique_ptr task, const v8::SourceLocation& location) { // The task can be posted from any V8 background worker thread, even when @@ -353,6 +359,11 @@ void PerIsolatePlatformData::PostTaskImpl(std::unique_ptr task, auto locked = foreground_tasks_.Lock(); if (flush_tasks_ == nullptr) return; + if (task->IsRecordReplayNonDeterministic() && v8::recordreplay::IsRecordingOrReplaying()) { + // For now we ignore non-deterministic tasks. This prevents the GC from working. + return; + } + v8::recordreplay::Assert("PerIsolatePlatformData::PostDelayedTask"); // All foreground tasks are treated as user blocking tasks. locked.Push(std::make_unique( std::move(task), v8::TaskPriority::kUserBlocking)); @@ -542,7 +553,11 @@ void PerIsolatePlatformData::RunForegroundTask(std::unique_ptr task) { if (isolate_->IsExecutionTerminating()) return; DebugSealHandleScope scope(isolate_); Environment* env = Environment::GetCurrent(isolate_); - if (env != nullptr) { + // Don't create an InternalCallbackScope when recording/replaying and the + // task runs non-deterministically, as the associated callbacks must run + // deterministically. + if (env != nullptr && + (!v8::recordreplay::IsRecordingOrReplaying() || !task->IsRecordReplayNonDeterministic())) { v8::HandleScope scope(isolate_); InternalCallbackScope cb_scope(env, Object::New(isolate_), { 0, 0 }, InternalCallbackScope::kNoFlags); @@ -571,6 +586,7 @@ void PerIsolatePlatformData::DeleteFromScheduledTasks(DelayedTask* task) { } void PerIsolatePlatformData::RunForegroundTask(uv_timer_t* handle) { + v8::recordreplay::Assert("PerIsolatePlatformData::RunForegroundTask"); DelayedTask* delayed = ContainerOf(&DelayedTask::timer, handle); delayed->platform_data->RunForegroundTask(std::move(delayed->task)); delayed->platform_data->DeleteFromScheduledTasks(delayed); @@ -747,6 +763,7 @@ std::shared_ptr NodePlatform::GetForegroundTaskRunner( double NodePlatform::MonotonicallyIncreasingTime() { // Convert nanos to seconds. + v8::recordreplay::AutoPassThroughEvents pt; return uv_hrtime() / 1e9; } @@ -773,7 +790,7 @@ v8::PageAllocator* NodePlatform::GetPageAllocator() { template TaskQueue::TaskQueue() - : lock_(), + : lock_(/* ordered */ true), tasks_available_(), outstanding_tasks_drained_(), outstanding_tasks_(0), From 453ef1070437806912dbcb152fc9fc1fe40fc926 Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Sat, 6 Jun 2026 16:14:02 -0400 Subject: [PATCH 03/40] Profile new Node 27 surfaces: instrument ncrypto::CSPRNG randomness chokepoint Covers all new randomness consumers (keygen, QUIC CIDs/tokens, DTLS, WebCrypto, randomUUID) at one point. See NEW-SURFACES.md for the full inventory of new non-deterministic subsystems (undici/fetch, QUIC, node:sqlite, FFI, fs.watch, ...) and recommended instrumentation/priority. --- deps/ncrypto/ncrypto.cc | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 38378b730aca..9578a9804901 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -561,7 +561,11 @@ BignumPointer BignumPointer::NewLShift(size_t length) { // ============================================================================ // Utility methods -bool CSPRNG(void* buffer, size_t length) { +// Replay port: V8 driver hook to record/replay raw bytes for determinism. +extern "C" void V8RecordReplayAssertBytes(const char* why, const void* buf, + size_t size); + +static bool CSPRNG_impl(void* buffer, size_t length) { auto buf = reinterpret_cast(buffer); do { if (1 == RAND_status()) { @@ -597,6 +601,17 @@ bool CSPRNG(void* buffer, size_t length) { return false; } +bool CSPRNG(void* buffer, size_t length) { + // Replay port: CSPRNG is the single randomness primitive in Node 27 — keygen, + // QUIC connection IDs/tokens, DTLS, WebCrypto, randomUUID, etc. all route + // through here. Recording/replaying its output at this chokepoint makes every + // consumer deterministic in one place (supersedes per-callsite hooks). + bool rv = CSPRNG_impl(buffer, length); + if (rv && length > 0) + V8RecordReplayAssertBytes("ncrypto::CSPRNG", buffer, length); + return rv; +} + int NoPasswordCallback(char* buf, int size, int rwflag, void* u) { return 0; } From 2ab21f37f4148b93c815b38258d8e059658140fe Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Sat, 6 Jun 2026 16:24:53 -0400 Subject: [PATCH 04/40] Guard new unsupported surfaces with InvalidateRecording node:sqlite (DatabaseSync::Open), fs.watch (FSEventWrap::Start), QUIC (Endpoint::Listen/Connect): invalidate the recording rather than diverge silently. Each has a TODO(replay-port) for real support. See NEW-SURFACES.md. --- src/fs_event_wrap.cc | 5 +++++ src/node_sqlite.cc | 6 ++++++ src/quic/endpoint.cc | 10 ++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/fs_event_wrap.cc b/src/fs_event_wrap.cc index 6d954e3de17c..e37dbe058729 100644 --- a/src/fs_event_wrap.cc +++ b/src/fs_event_wrap.cc @@ -157,6 +157,11 @@ void FSEventWrap::Start(const FunctionCallbackInfo& args) { wrap->encoding_ = ParseEncoding(env->isolate(), args[3], kDefaultEncoding); + // Replay port: OS file-change events arrive non-deterministically and are not + // yet recorded. Invalidate rather than diverge. TODO(replay-port): record and + // replay the fs-event stream to support fs.watch / --watch. + v8::recordreplay::InvalidateRecording("fs.watch (FSEvent) started"); + int err = uv_fs_event_init(wrap->env()->event_loop(), &wrap->handle_); if (err != 0) { return args.GetReturnValue().Set(err); diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 200d65ee1bf7..bbbff65c89c4 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -941,6 +941,12 @@ bool DatabaseSync::Open() { return false; } + // Replay port: node:sqlite is not yet modeled for record/replay. SQLite's VFS + // uses non-deterministic randomness (rowids/temp names), wall-clock time, and + // file-ordering. Invalidate rather than diverge silently. TODO(replay-port): + // override the SQLite VFS random/time hooks to support recording. + v8::recordreplay::InvalidateRecording("node:sqlite database opened"); + // TODO(cjihrig): Support additional flags. int default_flags = SQLITE_OPEN_URI; int flags = open_config_.get_read_only() diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc index 5a728a0a2a14..c0659dbb445d 100644 --- a/src/quic/endpoint.cc +++ b/src/quic/endpoint.cc @@ -1184,6 +1184,12 @@ void Endpoint::Listen(const Session::Options& options) { if (is_closed() || is_closing() || state_->listening == 1) return; DCHECK(!server_state_.has_value()); + // Replay port: QUIC (UDP datagram) network I/O is not yet recorded. Random + // connection IDs/tokens route through ncrypto::CSPRNG (covered), but the + // datagram traffic and timers are not. Invalidate rather than diverge. + // TODO(replay-port): record/replay QUIC endpoint send/recv. + v8::recordreplay::InvalidateRecording("QUIC Endpoint::Listen"); + // We need at least one key and one cert to complete the tls handshake on the // server. Why not make this an error? We could but it's not strictly // necessary. @@ -1226,6 +1232,10 @@ BaseObjectPtr Endpoint::Connect( const SocketAddress& remote_address, const Session::Options& options, std::optional session_ticket) { + // Replay port: QUIC network I/O is not yet recorded (see Endpoint::Listen). + // TODO(replay-port): record/replay QUIC endpoint send/recv. + v8::recordreplay::InvalidateRecording("QUIC Endpoint::Connect"); + // If starting fails, the endpoint will be destroyed. if (!Start()) return {}; From bb38eee08c9c6ac48318dd06b6f3e60707c6b4fe Mon Sep 17 00:00:00 2001 From: Brett Lamy Date: Sat, 6 Jun 2026 16:36:40 -0400 Subject: [PATCH 05/40] Apply Replay V8 instrumentation from replayio/chromium-v8 PR #282 (V8 15.1/M151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-pathed the PR's base..head diff to deps/v8/ and applied cleanly (git apply exit 0) onto Node 27's V8 15.1.42 — only 1 build off the PR base (15.1.43). 127 files (+6126/-101), incl. the recordreplay runtime (include/replayio.h, src/replay/*). This provides the v8::recordreplay API + V8RecordReplay* C shims that every node-core/libuv/ncrypto hook depends on. --- deps/v8/BUILD.gn | 7 + deps/v8/include/js_protocol.pdl | 32 + deps/v8/include/replayio-macros.h | 25 + deps/v8/include/replayio.h | 31 + deps/v8/include/v8-container.h | 5 +- deps/v8/include/v8-inspector.h | 9 + deps/v8/include/v8-object.h | 66 +- deps/v8/include/v8-platform.h | 6 + deps/v8/include/v8.h | 120 ++ deps/v8/src/api/api.cc | 1790 ++++++++++++++++- deps/v8/src/ast/ast-value-factory.h | 8 + deps/v8/src/ast/ast.h | 19 +- deps/v8/src/ast/scopes.cc | 18 + deps/v8/src/base/logging.cc | 30 + .../src/base/platform/condition-variable.cc | 1 + deps/v8/src/base/platform/elapsed-timer.h | 3 + deps/v8/src/base/platform/mutex.h | 2 +- deps/v8/src/base/platform/platform-darwin.cc | 6 + deps/v8/src/base/platform/platform-linux.cc | 8 + deps/v8/src/base/platform/platform.h | 3 + deps/v8/src/base/platform/time.cc | 7 + deps/v8/src/base/replayio.h | 33 + deps/v8/src/baseline/baseline-assembler.h | 2 + deps/v8/src/baseline/baseline-compiler.cc | 63 + .../baseline/x64/baseline-assembler-x64-inl.h | 4 + deps/v8/src/builtins/builtins-global.cc | 13 +- deps/v8/src/builtins/weak-ref.tq | 9 +- deps/v8/src/codegen/compilation-cache.cc | 11 +- deps/v8/src/codegen/compiler.cc | 145 +- deps/v8/src/codegen/external-reference.cc | 11 + deps/v8/src/codegen/external-reference.h | 2 + deps/v8/src/codegen/x64/assembler-x64.cc | 15 + .../v8/src/compiler/backend/code-generator.cc | 2 + .../v8/src/compiler/bytecode-graph-builder.cc | 103 + deps/v8/src/compiler/js-call-reducer.cc | 15 + .../js-native-context-specialization.cc | 20 + deps/v8/src/compiler/simplified-lowering.cc | 2 + deps/v8/src/compiler/simplified-operator.cc | 14 + deps/v8/src/compiler/simplified-operator.h | 1 + deps/v8/src/compiler/verifier.cc | 4 + deps/v8/src/debug/debug-interface.cc | 6 +- deps/v8/src/debug/debug-interface.h | 8 +- deps/v8/src/debug/debug-property-iterator.cc | 4 +- deps/v8/src/debug/debug-property-iterator.h | 4 +- deps/v8/src/debug/debug-scopes.cc | 3 + .../src/debug/debug-stack-trace-iterator.cc | 56 +- .../v8/src/debug/debug-stack-trace-iterator.h | 4 + deps/v8/src/debug/debug.cc | 1384 ++++++++++++- deps/v8/src/execution/execution.cc | 52 + deps/v8/src/execution/frames.cc | 15 +- deps/v8/src/execution/futex-emulation.cc | 14 + deps/v8/src/execution/isolate.cc | 90 +- deps/v8/src/execution/isolate.h | 15 + deps/v8/src/execution/messages.cc | 18 +- deps/v8/src/execution/microtask-queue.h | 7 + deps/v8/src/execution/stack-guard.cc | 5 + deps/v8/src/handles/handles.cc | 7 + deps/v8/src/heap/allocation-observer.cc | 2 + deps/v8/src/heap/concurrent-marking.cc | 2 + deps/v8/src/heap/cppgc/concurrent-marker.cc | 3 +- deps/v8/src/heap/cppgc/stats-collector.cc | 4 + deps/v8/src/heap/cppgc/sweeper.cc | 6 + deps/v8/src/heap/gc-tracer.cc | 5 + deps/v8/src/heap/heap-inl.h | 3 + deps/v8/src/heap/heap.cc | 10 + deps/v8/src/heap/heap.h | 3 + deps/v8/src/heap/incremental-marking-job.cc | 8 + deps/v8/src/heap/local-heap.cc | 8 + deps/v8/src/heap/mark-compact.cc | 2 +- deps/v8/src/heap/memory-reducer.cc | 5 + deps/v8/src/heap/memory-reducer.h | 2 + deps/v8/src/heap/paged-spaces.cc | 2 + deps/v8/src/inspector/injected-script.cc | 37 +- deps/v8/src/inspector/v8-console-message.cc | 8 + deps/v8/src/inspector/v8-console.cc | 2 + .../src/inspector/v8-debugger-agent-impl.cc | 99 +- .../v8/src/inspector/v8-debugger-agent-impl.h | 17 +- deps/v8/src/inspector/v8-debugger.h | 8 +- deps/v8/src/inspector/v8-inspector-impl.cc | 31 +- .../src/inspector/v8-inspector-session-impl.h | 12 + .../v8/src/inspector/v8-runtime-agent-impl.cc | 33 +- deps/v8/src/inspector/v8-runtime-agent-impl.h | 3 + deps/v8/src/inspector/value-mirror.cc | 63 +- deps/v8/src/inspector/value-mirror.h | 6 +- .../src/interpreter/bytecode-array-builder.cc | 123 ++ .../src/interpreter/bytecode-array-builder.h | 25 + .../src/interpreter/bytecode-array-writer.h | 2 + deps/v8/src/interpreter/bytecode-generator.cc | 125 +- deps/v8/src/interpreter/bytecode-generator.h | 8 + deps/v8/src/interpreter/bytecodes.h | 13 + .../src/interpreter/interpreter-generator.cc | 60 + .../libplatform/tracing/tracing-controller.cc | 6 + deps/v8/src/logging/counters-scopes.h | 6 + deps/v8/src/logging/metrics.h | 2 + deps/v8/src/maglev/maglev-graph-builder.cc | 43 + deps/v8/src/objects/elements.cc | 16 + deps/v8/src/objects/js-objects.cc | 3 +- deps/v8/src/objects/js-objects.h | 4 + deps/v8/src/objects/js-objects.tq | 1 + deps/v8/src/objects/keys.cc | 37 +- deps/v8/src/objects/keys.h | 17 +- deps/v8/src/objects/objects.cc | 15 +- deps/v8/src/objects/value-serializer.cc | 58 +- deps/v8/src/parsing/parse-info.cc | 10 +- deps/v8/src/parsing/parse-info.h | 5 +- deps/v8/src/parsing/parser-base.h | 14 +- deps/v8/src/parsing/parser.cc | 4 +- deps/v8/src/parsing/parser.h | 2 +- .../pending-compilation-error-handler.cc | 11 + deps/v8/src/parsing/preparser.h | 2 +- .../src/profiler/heap-snapshot-generator.cc | 9 + deps/v8/src/replay/replay-isolate-data.cc | 7 + deps/v8/src/replay/replay-isolate-data.h | 29 + .../v8/src/replay/replay-runtime-weak-refs.cc | 47 + deps/v8/src/replay/replayio.cc | 30 + deps/v8/src/replay/replayio.h | 26 + deps/v8/src/replay/weak-refs.cc | 38 + deps/v8/src/replay/weak-refs.h | 22 + deps/v8/src/runtime/runtime-compiler.cc | 2 + deps/v8/src/runtime/runtime-debug.cc | 610 ++++++ deps/v8/src/runtime/runtime-internal.cc | 23 + deps/v8/src/runtime/runtime-promise.cc | 2 + deps/v8/src/runtime/runtime.h | 10 +- deps/v8/src/trap-handler/handler-shared.cc | 3 + deps/v8/src/utils/utils-inl.h | 3 + deps/v8/src/wasm/wasm-code-manager.h | 8 + .../inspector_protocol/crdtp/dispatch.cc | 5 +- 127 files changed, 6126 insertions(+), 101 deletions(-) create mode 100644 deps/v8/include/replayio-macros.h create mode 100644 deps/v8/include/replayio.h create mode 100644 deps/v8/src/base/replayio.h create mode 100644 deps/v8/src/replay/replay-isolate-data.cc create mode 100644 deps/v8/src/replay/replay-isolate-data.h create mode 100644 deps/v8/src/replay/replay-runtime-weak-refs.cc create mode 100644 deps/v8/src/replay/replayio.cc create mode 100644 deps/v8/src/replay/replayio.h create mode 100644 deps/v8/src/replay/weak-refs.cc create mode 100644 deps/v8/src/replay/weak-refs.h diff --git a/deps/v8/BUILD.gn b/deps/v8/BUILD.gn index 167d9a9003d8..5fdec86d5a0c 100644 --- a/deps/v8/BUILD.gn +++ b/deps/v8/BUILD.gn @@ -6342,6 +6342,13 @@ v8_cluster_source_set("v8_base_without_compiler") { "src/zone/type-stats.cc", "src/zone/zone-segment.cc", "src/zone/zone.cc", + "src/replay/replay-isolate-data.cc", + "src/replay/replay-isolate-data.h", + "src/replay/replayio.cc", + "src/replay/replayio.h", + "src/replay/replay-runtime-weak-refs.cc", + "src/replay/weak-refs.cc", + "src/replay/weak-refs.h", "third_party/siphash/halfsiphash.cc", ] diff --git a/deps/v8/include/js_protocol.pdl b/deps/v8/include/js_protocol.pdl index bc86332d869b..1fe831586d3d 100644 --- a/deps/v8/include/js_protocol.pdl +++ b/deps/v8/include/js_protocol.pdl @@ -107,6 +107,8 @@ domain Debugger # Deprecated in favor of using the `location.scriptId` to resolve the URL via a previously # sent `Debugger.scriptParsed` event. deprecated string url + # Execution context associated with this frame. + Runtime.ExecutionContextId contextId # Scope chain for this call frame. array of Scope scopeChain # `this` object for this call frame. @@ -572,6 +574,28 @@ domain Debugger # Actual breakpoint location. Location location + command getCallFrames + parameters + # Maximum number of frames to return. + optional integer maxFrames + # Don't generate scopes or "this" object for returned frames. + optional boolean noContents + # Symbolic group name that can be used to release multiple objects. + optional string objectGroup + returns + array of CallFrame callFrames + + command getTopFrameLocation + returns + optional Location location + + command getPendingException + parameters + # Symbolic group name that can be used to release multiple objects. + optional string objectGroup + returns + optional Runtime.RemoteObject exception + # Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. event paused parameters @@ -1157,6 +1181,8 @@ domain Runtime experimental optional DeepSerializedValue deepSerializedValue # Unique object identifier (for non-primitive values). optional RemoteObjectId objectId + # Any available persistent object identifier when recording/replaying. + optional string persistentId # Preview containing abbreviated property values. Specified for `object` type values only. experimental optional ObjectPreview preview experimental optional CustomPreview customPreview @@ -1589,6 +1615,8 @@ domain Runtime # Size in bytes of backing storage for array buffers and external strings. number backingStorageSize + type KeyIterationIndex extends integer + # Returns properties of a given object. Object group of the result is inherited from the target # object. command getProperties @@ -1605,6 +1633,10 @@ domain Runtime experimental optional boolean generatePreview # If true, returns non-indexed properties only. experimental optional boolean nonIndexedPropertiesOnly + # If > 0, returns up to `pageSize` properties only. + optional KeyIterationIndex pageSize + # If pageIndex > 0 and pageSize > 0, returns properties starting at pageIndex * pageSize. + optional KeyIterationIndex pageIndex returns # Object properties. array of PropertyDescriptor result diff --git a/deps/v8/include/replayio-macros.h b/deps/v8/include/replayio-macros.h new file mode 100644 index 000000000000..5a619253b2ab --- /dev/null +++ b/deps/v8/include/replayio-macros.h @@ -0,0 +1,25 @@ +// Copyright (c) 2024 Record Replay Inc. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// API for interacting with the record/replay driver. +// Some parts are still in v8.h and still need to be migrated. + + +#ifndef INCLUDE_REPLAYIO_MACROS_H_ +#define INCLUDE_REPLAYIO_MACROS_H_ + +// Use this to wrap Asserts on non-trivial data, to avoid the +// overhead of argument evaluation when Asserts are disabled. +#define REPLAY_ASSERT(format, ...) \ + if (recordreplay::HasAsserts()) \ + recordreplay::Assert(format, ##__VA_ARGS__); \ + static_assert(true, "require semicolon") + +// Same as |REPLAY_ASSERT| but won't Assert when Events are disallowed. +#define REPLAY_ASSERT_MAYBE_EVENTS_DISALLOWED(format, ...) \ + if (recordreplay::HasAsserts() && !recordreplay::AreEventsDisallowed()) \ + recordreplay::Assert(format, ##__VA_ARGS__); \ + static_assert(true, "require semicolon") + +#endif // INCLUDE_REPLAYIO_MACROS_H_ diff --git a/deps/v8/include/replayio.h b/deps/v8/include/replayio.h new file mode 100644 index 000000000000..db681621110a --- /dev/null +++ b/deps/v8/include/replayio.h @@ -0,0 +1,31 @@ +// Copyright (c) 2024 Record Replay Inc. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// API for interacting with the record/replay driver. +// Some parts are still in v8.h and still need to be migrated. + + +#ifndef INCLUDE_RECORD_REPLAY_H_ +#define INCLUDE_RECORD_REPLAY_H_ + +#include "v8.h" + +namespace v8 { +namespace replayio { + +struct AutoPassThroughEvents { + AutoPassThroughEvents() { v8::recordreplay::BeginPassThroughEvents(); } + ~AutoPassThroughEvents() { v8::recordreplay::EndPassThroughEvents(); } +}; + +struct AutoDisallowEvents { + AutoDisallowEvents() { v8::recordreplay::BeginDisallowEvents(); } + AutoDisallowEvents(const char* label) { v8::recordreplay::BeginDisallowEventsWithLabel(label); } + ~AutoDisallowEvents() { v8::recordreplay::EndDisallowEvents(); } +}; + +} // namespace replayio +} // namespace v8 + +#endif // INCLUDE_RECORD_REPLAY_H_ diff --git a/deps/v8/include/v8-container.h b/deps/v8/include/v8-container.h index 380999e5ee45..fd11f68ed4e6 100644 --- a/deps/v8/include/v8-container.h +++ b/deps/v8/include/v8-container.h @@ -123,7 +123,8 @@ class V8_EXPORT Map : public Object { * Returns an array of length Size() * 2, where index N is the Nth key and * index N + 1 is the Nth value. */ - Local AsArray() const; + Local AsArray(const v8::KeyIterationParams* params = + v8::KeyIterationParams::Default()) const; /** * Creates a new empty Map. @@ -159,7 +160,7 @@ class V8_EXPORT Set : public Object { /** * Returns an array of the keys in this Set. */ - Local AsArray() const; + Local AsArray(const v8::KeyIterationParams* params = v8::KeyIterationParams::Default()) const; /** * Creates a new empty Set. diff --git a/deps/v8/include/v8-inspector.h b/deps/v8/include/v8-inspector.h index 05e9edadfdc8..99ad198a1ead 100644 --- a/deps/v8/include/v8-inspector.h +++ b/deps/v8/include/v8-inspector.h @@ -193,6 +193,9 @@ class V8_EXPORT V8InspectorSession { virtual void setSkipAllPauses(bool) = 0; virtual void resume(bool setTerminateOnResume = false) = 0; virtual void stepOver() = 0; + + virtual v8::MaybeLocal getArgumentsOfCallFrame(StringView callFrameId) = 0; + virtual std::vector> searchInTextByLines(StringView text, StringView query, bool caseSensitive, bool isRegex) = 0; @@ -202,6 +205,12 @@ class V8_EXPORT V8InspectorSession { v8::Local, v8::Local, StringView groupName, bool generatePreview) = 0; + // Replay edit: Return objectId + virtual std::u16string wrapObjectGetObjectId(v8::Local, + v8::Local, + StringView groupName, + bool generatePreview) = 0; + virtual bool unwrapObject(std::unique_ptr* error, StringView objectId, v8::Local*, v8::Local*, diff --git a/deps/v8/include/v8-object.h b/deps/v8/include/v8-object.h index f283857a4d18..c8e14bd55a64 100644 --- a/deps/v8/include/v8-object.h +++ b/deps/v8/include/v8-object.h @@ -260,6 +260,69 @@ enum class KeyConversionMode { kConvertToString, kKeepNumbers, kNoNumbers }; */ enum class IntegrityLevel { kFrozen, kSealed }; +using KeyIterationIndex = int; + +/** + * [RecordReplay] + * Used to paginate keys for performance reasons (RUN-1315). + * This is currently only implemented as part of the debugger's + * `Runtime.getProperties` command. It is also not a guarantee on pagination, + * but purely a performance improvement. We apply these params in many scenarios + * where large key sets would slow things down a lot. + * WARNING: |page_index_| has not been fully implemented. + * NOTE: In the future, this can be updated to encapsulate all key query + * parameters in a single object (`KeyCollectionMode`, `IndexFilter` etc.). + */ +class KeyIterationParams { + KeyIterationIndex page_size_; + /** WARNING: |page_index_| is not fully implemented. */ + KeyIterationIndex page_index_; + public: + /** + * Create new KeyIterationParams to limit iteration to between |page_index| and + * |page_index + page_size|. + */ + KeyIterationParams(KeyIterationIndex page_size, KeyIterationIndex page_index) + : page_size_(page_size) , page_index_(page_index) {} + + /** + * The min of page_size_ or target size. + * Ignores page_size_ if no parameters are given. + */ + KeyIterationIndex PageSize(KeyIterationIndex numberOfElements) const { + return (*this && page_size_ < numberOfElements) ? page_size_ + : numberOfElements; + } + + /** + * First index of iteration. + */ + KeyIterationIndex KeyFirstIndex() const { return page_index_ * page_size_; } + + /** + * Final index of iteration + 1, indicating end of iteration. + */ + KeyIterationIndex KeyEndIndex(KeyIterationIndex numberOfElements) const { + return (*this && (KeyFirstIndex() + page_size_) < numberOfElements) + ? KeyFirstIndex() + page_size_ + : numberOfElements; + } + + /** + * Whether parameters are given. + * If no parameters are given, key iteration will iterate all keys at once. + */ + operator bool() const { return page_size_ > 0; } + + /** + * Default (i.e. empty) key iteration params. + */ + static const KeyIterationParams* Default() { + static KeyIterationParams params(0, 0); + return ¶ms; + } +}; + /** * A JavaScript object (ECMA-262, 4.3.3) */ @@ -895,7 +958,8 @@ class V8_EXPORT Object : public Value { * For other types, this will return an empty MaybeLocal (without * scheduling an exception). */ - MaybeLocal PreviewEntries(bool* is_key_value); + MaybeLocal PreviewEntries(bool* is_key_value, + const v8::KeyIterationParams* params = KeyIterationParams::Default()); static Local New(Isolate* isolate); diff --git a/deps/v8/include/v8-platform.h b/deps/v8/include/v8-platform.h index 8f42ac0c8788..0df19d389542 100644 --- a/deps/v8/include/v8-platform.h +++ b/deps/v8/include/v8-platform.h @@ -52,6 +52,12 @@ class Task { virtual ~Task() = default; virtual void Run() = 0; + + /** + * When recording/replaying, whether this task might not consistently occur + * between recording and replaying. + */ + virtual bool IsRecordReplayNonDeterministic() const { return false; } }; /** diff --git a/deps/v8/include/v8.h b/deps/v8/include/v8.h index cc23d207aea6..04f3b06529c7 100644 --- a/deps/v8/include/v8.h +++ b/deps/v8/include/v8.h @@ -17,6 +17,7 @@ #include #include +#include #include @@ -62,6 +63,8 @@ #include "v8-wasm.h" // NOLINT(build/include_directory) #include "v8config.h" // NOLINT(build/include_directory) +#include "replayio-macros.h" + // We reserve the V8_* prefix for macros defined in V8 public API and // assume there are no name conflicts with the embedder's code. @@ -82,6 +85,123 @@ class Platform; * \example process.cc */ +bool IsMainThread(); + +// Static container class for record/replay methods. +class V8_EXPORT recordreplay { + public: + +static void SetRecordingOrReplaying(void* handle); +static bool IsRecordingOrReplaying(const char* feature = nullptr, + const char* subfeature = nullptr); +static bool IsRecording(); +static bool IsReplaying(); +static const char* GetRecordingId(); + +static bool IsARMRecording(); + +static bool FeatureEnabled(const char* feature, const char* subfeature = nullptr); +static bool HasDisabledFeatures(); + +static char* ReadAssetFileContents(const char* path, size_t* size); +static void Print(const char* format, ...); +static void Diagnostic(const char* format, ...); +static void CommandDiagnostic(const char* format, ...); +static void CommandDiagnosticTrace(const char* format, ...); +static void Warning(const char* format, ...); +static void Trace(const char* format, ...); +static void Crash(const char* format, ...); +static bool HadMismatch(); +static bool HasAsserts(); +static void AssertRaw(const char* format, ...); +static void Assert(const char* format, ...); +static void AssertMaybeEventsDisallowed(const char* format, ...); +static void AssertBytes(const char* why, const void* buf, size_t size); +static bool AreAssertsDisabled(); + +static uintptr_t RecordReplayValue(const char* why, uintptr_t v); +static void RecordReplayBytes(const char* why, void* buf, size_t size); +static void RecordReplayString(const char* why, std::string& str); + +static size_t CreateOrderedLock(const char* name); +static void OrderedLock(int lock); +static void OrderedUnlock(int lock); + +static void InvalidateRecording(const char* why); +static void NewCheckpoint(); + +static void BeginPassThroughEvents(); +static void EndPassThroughEvents(); +static bool AreEventsPassedThrough(const char* why = nullptr); + +static void BeginDisallowEvents(); +static void BeginDisallowEventsWithLabel(const char* label); +static void EndDisallowEvents(); + +// A "why" string should be used whenever there are substantive behavior changes +// resulting from this check. +static bool AreEventsDisallowed(const char* why = nullptr); + +// A "why" string should be used whenever there are substantive behavior changes +// resulting from this check. +static bool IsInReplayCode(const char* why = nullptr); + +static bool HasDivergedFromRecording(); +static bool AllowSideEffects(); + +static int NewDependencyGraphNode(const char* json); +static void AddDependencyGraphEdge(int source, int target, const char* json); +static void BeginDependencyExecution(int node); +static void EndDependencyExecution(); + +struct AutoDependencyExecution { + AutoDependencyExecution(int node) { + BeginDependencyExecution(node); + } + ~AutoDependencyExecution() { + EndDependencyExecution(); + } +}; + +static void BeginAssertBufferAllocations(const char* issueLabel = ""); +static void EndAssertBufferAllocations(); + +struct AutoOrderedLock { + AutoOrderedLock(int id) : id_(id) { OrderedLock(id_); } + ~AutoOrderedLock() { OrderedUnlock(id_); } + int id_; +}; + +struct AutoAssertMaybeEventsDisallowed { + AutoAssertMaybeEventsDisallowed(const char* format, ...); + ~AutoAssertMaybeEventsDisallowed(); + std::string msg_; +}; + +struct AssertBufferAllocationState { + size_t enabled = 0; + std::string issueLabel = ""; +}; + + +// RAII class to enable recording assertions on dynamic-length buffer +// allocations. Used to track down the allocation causing mismatched message +// sizes when replaying. +// TODO: Merge this with the similar `AuxtoRecordReplayAssertBufferAllocations` +// in mojo/public/cpp/bindings/lib/buffer.cc (for main-thread only). +struct AutoAssertBufferAllocations { + static AssertBufferAllocationState* GetState(); + + AutoAssertBufferAllocations(const char* issueLabel = ""); + ~AutoAssertBufferAllocations(); +}; + +static void RegisterPointer(const char* name, const void* ptr); +static void UnregisterPointer(const void* ptr); +static int PointerId(const void* ptr); +static void* IdPointer(int id); + +}; // class recordreplay } // namespace v8 diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc index 315659e78fef..f8225f48fd11 100644 --- a/deps/v8/src/api/api.cc +++ b/deps/v8/src/api/api.cc @@ -45,6 +45,7 @@ #include "src/base/template-utils.h" #include "src/base/utils/random-number-generator.h" #include "src/base/vector.h" +#include "src/base/vector.h" #include "src/builtins/accessors.h" #include "src/builtins/builtins-promise.h" #include "src/builtins/builtins-utils.h" @@ -172,6 +173,13 @@ #include #include +#if !V8_OS_WIN +#include +#include +#endif + +extern const char* gCrashReason; + #if V8_ENABLE_WEBASSEMBLY #include "include/v8-wasm-trap-handler-posix.h" #include "src/trap-handler/handler-inside-posix.h" @@ -238,6 +246,12 @@ static ScriptOrigin GetScriptOriginForScript( // --- E x c e p t i o n B e h a v i o r --- +static __attribute__((noinline)) void BusyWait() { + fprintf(stderr, "Busy-waiting ... (pid %d)\n", getpid()); + volatile int x = 1; + while (x) {} +} + // When V8 cannot allocate memory FatalProcessOutOfMemory is called. The default // OOM error handler is called and execution is stopped. void i::V8::FatalProcessOutOfMemory(i::Isolate* i_isolate, const char* location, @@ -293,6 +307,9 @@ void Utils::ReportApiFailure(const char* location, const char* message) { if (callback == nullptr) { base::OS::PrintError("\n#\n# Fatal error in %s\n# %s\n#\n\n", location, message); + if (getenv("RECORD_REPLAY_WAIT_AT_FATAL_ERROR")) { + BusyWait(); + } base::OS::Abort(); } else { callback(location, message); @@ -2290,6 +2307,14 @@ Local Module::GetModuleRequests() const { } } + // TODO: IsInReplayCode (RUN-1502) + v8::recordreplay::AssertMaybeEventsDisallowed( + "JS Script::Run %d", + fun->shared().script().IsScript() + ? i::Script::cast(fun->shared().script()).id() + : 0 + ); + Location Module::SourceOffsetToLocation(int offset) const { auto self = Utils::OpenDirectHandle(this); i::Isolate* i_isolate = i::Isolate::Current(); @@ -2425,6 +2450,9 @@ MaybeLocal Module::Evaluate(Local context) { Utils::ApiCheck(self->status() >= i::Module::kLinked, "Module::Evaluate", "Expected instantiated module"); + // TODO: IsInReplayCode (RUN-1502) + v8::recordreplay::AssertMaybeEventsDisallowed("JS Module::Evaluate %d", ScriptId()); + return api_scope.EscapeMaybe(i::Module::Evaluate(i_isolate, self)); } @@ -2578,6 +2606,55 @@ i::ScriptDetails GetScriptDetails( } // namespace +static const char* RecordReplayReplaceSourceContents(const char* contents); + +namespace internal { + +// If we are compiling a script while replaying that replaces another one, +// the ID of the script being replaced. Main thread only. +int gReplaceSourceContentsScriptId; + +MaybeHandle +ReplayingReplaceScriptContents(Isolate* isolate, Handle source) { + if (!recordreplay::IsReplaying() || !IsMainThread()) { + return MaybeHandle(); + } + + std::unique_ptr contents = source->ToCString(); + const char* new_contents = RecordReplayReplaceSourceContents(contents.get()); + if (!new_contents) { + return MaybeHandle(); + } + + return isolate->factory()->NewStringFromUtf8(base::CStrVector(new_contents)).ToHandleChecked(); +} + +MaybeHandle +ReplayingMaybeReplaceScript(Isolate* isolate, + MaybeHandle maybe_function_info, + const ScriptDetails& script_details, + Handle source) { + MaybeHandle new_source = ReplayingReplaceScriptContents(isolate, source); + + if (new_source.is_null() || maybe_function_info.is_null()) { + return maybe_function_info; + } + + CHECK(!gReplaceSourceContentsScriptId); + gReplaceSourceContentsScriptId = Script::cast(maybe_function_info.ToHandleChecked()->script()).id(); + + maybe_function_info = Compiler::GetSharedFunctionInfoForScript( + isolate, new_source.ToHandleChecked(), script_details, + ScriptCompiler::kNoCompileOptions, + ScriptCompiler::kNoCacheNoReason, + NOT_NATIVES_CODE); + + gReplaceSourceContentsScriptId = 0; + return maybe_function_info; +} + +} // namespace internal + MaybeLocal ScriptCompiler::CompileUnboundInternal( Isolate* v8_isolate, Source* source, CompileOptions options, NoCacheReason no_cache_reason) { @@ -2650,6 +2727,8 @@ MaybeLocal ScriptCompiler::CompileUnboundScript( return CompileUnboundInternal(v8_isolate, source, options, no_cache_reason); } + maybe_function_info = i::ReplayingMaybeReplaceScript(i_isolate, maybe_function_info, script_details, str); + MaybeLocal