diff --git a/.gitignore b/.gitignore index 4774ebf0812..94b360562c7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,10 +55,6 @@ venv *.core iwyu.dat -/src/mongo/*/*Debug*/ -/src/mongo/*/*/*Debug*/ -/src/mongo/*/*Release*/ -/src/mongo/*/*/*Release*/ /src/ipch /src/mongo/*/ipch /src/mongo/*/*/ipch diff --git a/BUILD.bazel b/BUILD.bazel index 99cc8a480c0..894bbeed0b3 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -34,6 +34,7 @@ mongo_js_library( ":node_modules/@eslint/js", ":node_modules/eslint-plugin-mongodb", ":node_modules/globals", + "//src/mongo/shell/debugger/vscode:eslint", ], ) diff --git a/eslint.config.mjs b/eslint.config.mjs index 578ab9a468f..f61f55e206b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,11 +1,12 @@ import {FlatCompat} from "@eslint/eslintrc"; -import eslint from "@eslint/js"; import js from "@eslint/js"; import {default as mongodb_plugin} from "eslint-plugin-mongodb"; import globals from "globals"; import path from "node:path"; import {fileURLToPath} from "node:url"; +import vscodeDebuggerConfig from "./src/mongo/shell/debugger/vscode/eslint.config.mjs"; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ @@ -403,4 +404,8 @@ export default [ "prefer-spread": 2, }, }, + { + files: ["src/mongo/shell/debugger/vscode/**/*.{js,mjs}"], + ...vscodeDebuggerConfig, + }, ]; diff --git a/src/mongo/shell/debugger/README.md b/src/mongo/shell/debugger/README.md index b47cbd83490..06183483aeb 100644 --- a/src/mongo/shell/debugger/README.md +++ b/src/mongo/shell/debugger/README.md @@ -1,13 +1,13 @@ # JS Debugging in the MongoDB Shell - Use the `--shellJSDebugMode` flag for resmoke (or the `--jsDebugMode` flag directly on the mongo shell) to trigger an interactive debug prompt when `debugger` statements are hit in JS test code. Sample JS Test: + ```js let x = 42; let y = ["a", 15, [1, 2, 3]]; -let z = {id: ObjectId(), value:42}; +let z = {id: ObjectId(), value: 42}; debugger; assert.eq(x, 7); @@ -16,27 +16,33 @@ print("Test Passed!"); ``` Running this test from the shell will fail since `x != 7`, and the `debugger` is a no-op (does not have any callback handler): + ```bash ./bazel-bin/install/bin/mongo --nodb jstests/my_test.js ``` + Output: + ``` Test Passed! ``` Run with the `--jsDebugMode` flag: + ```bash ./bazel-bin/install/bin/mongo --nodb --jsDebugMode jstests/my_test.js ``` Should pause first at line 5, and prompt the user for input: + ``` JSDEBUG> JavaScript execution paused in 'debugger' statement. JSDEBUG> Type 'dbcont' to continue -JSDEBUG@jstests/my_test.js:5> +JSDEBUG@jstests/my_test.js:5> ``` Inspect variables: + ``` JSDEBUG@jstests/my_test.js:5> x 42 @@ -47,12 +53,14 @@ JSDEBUG@jstests/my_test.js:5> z ``` Modify `x` so it passes its upcoming assertion: + ``` JSDEBUG@jstests/my_test.js:5> x = 7 7 ``` The `q` variable does not exist yet, so we can set it to pass its upcoming assertion: + ``` JSDEBUG@jstests/my_test.js:5> q ReferenceError: q is not defined @@ -61,22 +69,25 @@ foo ``` Send a `dbcont` command to continue execution, and now the test passes! + ``` JSDEBUG@jstests/my_test.js:5> dbcont JSDEBUG> Continuing execution... Test Passed! -All pids dead / alive (0): +All pids dead / alive (0): Searching for files in: /home/ubuntu/mongo ``` ## Resmoke Use the `--shellJSDebugMode` flag in resmoke to stop on debugger statements: + ```bash buildscripts/resmoke.py run --suites=no_passthrough --shellJSDebugMode jstests/my_test.js ``` Update variables `x` and `q` to repair the failing assertions: + ``` JSDEBUG> JavaScript execution paused in 'debugger' statement. JSDEBUG> Type 'dbcont' to continue diff --git a/src/mongo/shell/debugger/vscode/.gitignore b/src/mongo/shell/debugger/vscode/.gitignore new file mode 100644 index 00000000000..c2658d7d1b3 --- /dev/null +++ b/src/mongo/shell/debugger/vscode/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/src/mongo/shell/debugger/vscode/BUILD.bazel b/src/mongo/shell/debugger/vscode/BUILD.bazel new file mode 100644 index 00000000000..311dd7c8cee --- /dev/null +++ b/src/mongo/shell/debugger/vscode/BUILD.bazel @@ -0,0 +1,15 @@ +load("//bazel:mongo_js_rules.bzl", "mongo_js_library") + +package(default_visibility = ["//visibility:public"]) + +mongo_js_library( + name = "all_javascript_files", + srcs = glob([ + "*.js", + ]), +) + +mongo_js_library( + name = "eslint", + srcs = ["eslint.config.mjs"], +) diff --git a/src/mongo/shell/debugger/vscode/README.md b/src/mongo/shell/debugger/vscode/README.md new file mode 100644 index 00000000000..72e2713ce60 --- /dev/null +++ b/src/mongo/shell/debugger/vscode/README.md @@ -0,0 +1,38 @@ +# VSCode Extension for Debugging JS in the Mongo Shell + +Install the extension: + +```bash +./src/mongo/shell/debugger/vscode/install.sh +``` + +Add a "mongo-shell" type configuration in your `~/.vscode/launch.json` file: + +```json +{ + "version": "0.1.0", + "configurations": [ + { + "type": "mongo-shell", + "request": "attach", + "name": "Attach to MongoDB Shell", + "debugPort": 9229 + } + ] +} +``` + +Restart VSCode, or use CMD+SHIFT+P and select "Developer: Reload Window". + +## Usage + +1. Open a .js test file in VSCode +2. Add a breakpoint next to the line number (a red dot) +3. Press F5 to start the (VSCode) debug server, ready to "attach" to a debug shell + > You should see the following in the "Debug Console" of VSCode: + ``` + Debug server listening on port 9229 + Waiting for mongo shell to connect on port 9229... + Use resmoke's --shellJSDebugMode flag when running a JS test file to stop on breakpoints. + ``` +4. [TODO] Now you can run resmoke with the `--shellJSDebugMode` flag to stop on the breakpoints diff --git a/src/mongo/shell/debugger/vscode/adapter.js b/src/mongo/shell/debugger/vscode/adapter.js new file mode 100644 index 00000000000..7041f227b7f --- /dev/null +++ b/src/mongo/shell/debugger/vscode/adapter.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +/** + * Debug Adapter entry point for MongoDB Shell JS Debugger + * This executable is invoked by VSCode when starting a debug session + */ + +console.log("Starting MongoDB Shell JS Debug Adapter..."); + +const {MongoShellDebugSession} = require("./session"); +const {DebugSession} = require("vscode-debugadapter"); + +// Start the debug session +DebugSession.run(MongoShellDebugSession); diff --git a/src/mongo/shell/debugger/vscode/eslint.config.mjs b/src/mongo/shell/debugger/vscode/eslint.config.mjs new file mode 100644 index 00000000000..5643e555640 --- /dev/null +++ b/src/mongo/shell/debugger/vscode/eslint.config.mjs @@ -0,0 +1,23 @@ +// This folder contains JS intended to run in a Node-based environment, not the Mongo shell. + +import globals from "globals"; + +export default { + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { + ...globals.node, + ...globals.es2021, + }, + }, + rules: { + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + caughtErrors: "all", + }, + ], + }, +}; diff --git a/src/mongo/shell/debugger/vscode/extension.js b/src/mongo/shell/debugger/vscode/extension.js new file mode 100644 index 00000000000..266a6cf90df --- /dev/null +++ b/src/mongo/shell/debugger/vscode/extension.js @@ -0,0 +1,45 @@ +/** + * VSCode Extension for MongoDB Shell Debugger + * + * Registers the 'mongo-shell' debug type and provides configuration. + */ + +const vscode = require("vscode"); +const path = require("path"); + +// Extension entry point +function activate(context) { + // Register debug config provider + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider("mongo-shell", { + provideDebugConfigurations, + }), + ); + + // Register debug adapter factory + context.subscriptions.push( + vscode.debug.registerDebugAdapterDescriptorFactory("mongo-shell", { + createDebugAdapterDescriptor: (session) => createDebugAdapter(context, session), + }), + ); +} + +function deactivate() {} + +function provideDebugConfigurations(_folder) { + return [ + { + type: "mongo-shell", + request: "attach", + name: "Attach to MongoDB Shell", + debugPort: 9229, + }, + ]; +} + +function createDebugAdapter(context, _session) { + const adapterPath = path.join(context.extensionPath, "adapter.js"); + return new vscode.DebugAdapterExecutable("node", [adapterPath]); +} + +module.exports = {activate, deactivate}; diff --git a/src/mongo/shell/debugger/vscode/install.sh b/src/mongo/shell/debugger/vscode/install.sh new file mode 100755 index 00000000000..77b0fea715e --- /dev/null +++ b/src/mongo/shell/debugger/vscode/install.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +cd $MONGO_REPO +cd src/mongo/shell/debugger/vscode + +npm install + +# Get version from package.json +VERSION=$(node -p "require('./package.json').version") +PUBLISHER=$(node -p "require('./package.json').publisher") +EXT_NAME=$(node -p "require('./package.json').name") + +# Clean up old versions of the extension +rm -rf ~/.vscode/extensions/$PUBLISHER.$EXT_NAME-* +rm -rf ~/.vscode-server/extensions/$PUBLISHER.$EXT_NAME-* + +# Detect if using VS Code Remote SSH or local +if [ -d ~/.vscode-server/extensions ]; then + EXT_DIR=~/.vscode-server/extensions/$PUBLISHER.$EXT_NAME-$VERSION +else + EXT_DIR=~/.vscode/extensions/$PUBLISHER.$EXT_NAME-$VERSION +fi + +# Create extension directory and copy all necessary files +mkdir -p $EXT_DIR + +cp ./package.json $EXT_DIR/ +cp ./README.md $EXT_DIR/ + +cp ./adapter.js $EXT_DIR/ +cp ./extension.js $EXT_DIR/ +cp ./session.js $EXT_DIR/ + +cp -r ./node_modules $EXT_DIR/ + +echo "Extension installed to $EXT_DIR" +echo "Restart VSCode or run 'Developer: Reload Window' to activate the extension" + +cd $MONGO_REPO diff --git a/src/mongo/shell/debugger/vscode/package.json b/src/mongo/shell/debugger/vscode/package.json new file mode 100644 index 00000000000..5861681828d --- /dev/null +++ b/src/mongo/shell/debugger/vscode/package.json @@ -0,0 +1,83 @@ +{ + "name": "mongo-shell-debugger", + "displayName": "MongoDB Shell Debugger", + "description": "Debug JavaScript code in MongoDB Shell (MozJS)", + "version": "0.1.0", + "publisher": "mongodb", + "repository": { + "type": "git", + "url": "https://github.com/10gen/mongo.git" + }, + "engines": { + "vscode": "^1.75.0" + }, + "categories": [ + "Debuggers" + ], + "keywords": [ + "mongodb", + "javascript", + "debugger", + "mongo-shell" + ], + "main": "./extension.js", + "activationEvents": [ + "onDebug", + "onDebugResolve:mongo-shell", + "onDebugDynamicConfigurations:mongo-shell" + ], + "contributes": { + "breakpoints": [ + { + "language": "javascript" + } + ], + "debuggers": [ + { + "type": "mongo-shell", + "label": "Mongo Shell", + "program": "./adapter.js", + "runtime": "node", + "configurationAttributes": { + "attach": { + "required": [ + "debugPort" + ], + "properties": { + "debugPort": { + "type": "number", + "description": "Port of running debug server", + "default": 9229 + }, + "trace": { + "type": "boolean", + "description": "Enable logging of the Debug Adapter Protocol", + "default": false + } + } + } + }, + "initialConfigurations": [ + ], + "configurationSnippets": [ + { + "label": "MongoDB Shell: Attach", + "description": "Attach to running MongoDB Shell debug server", + "body": { + "type": "mongo-shell", + "request": "attach", + "name": "Attach to MongoDB Shell", + "debugPort": 9229 + } + } + ], + "variables": { + "AskForProgramName": "extension.mongo-shell-debugger.getProgramName" + } + } + ] + }, + "devDependencies": { + "@types/vscode": "^1.75.0" + } +} diff --git a/src/mongo/shell/debugger/vscode/session.js b/src/mongo/shell/debugger/vscode/session.js new file mode 100644 index 00000000000..870349ec52a --- /dev/null +++ b/src/mongo/shell/debugger/vscode/session.js @@ -0,0 +1,94 @@ +/** + * MongoDB Shell JS Debug Session + * + * Implements the Debug Adapter Protocol for debugging JavaScript in MongoDB Shell. + * https://microsoft.github.io/debug-adapter-protocol/overview + */ + +const { + DebugSession, + OutputEvent, + // https://github.com/microsoft/vscode-debugadapter-node/tree/main/adapter +} = require("vscode-debugadapter"); +const net = require("net"); + +class MongoShellDebugSession extends DebugSession { + constructor() { + super(); + this.setDebuggerLinesStartAt1(true); + this.setDebuggerColumnsStartAt1(true); + + // State + this.debugConnection = null; + this.debugServer = null; + this.connected = false; + } + + // Attach - start a debug server, listening for a mongo shell to attach to + async attachRequest(response, args) { + try { + await this.startDebugServer(args.debugPort || 9229); + this.log(`Waiting for mongo shell to connect on port ${args.debugPort || 9229}...\n`); + this.log("Use resmoke's --shellJSDebugMode flag when running a JS test file to stop on breakpoints.\n"); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse(response, 1000, `Failed to attach: ${err.message}`); + } + } + + // Start TCP server to accept connections from mongo shell + startDebugServer(port) { + return new Promise((resolve, reject) => { + this.debugServer = net.createServer((socket) => { + this.debugConnection = socket; + this.connected = true; + this.setupDebugConnection(); + }); + + this.debugServer.listen(port, "localhost", () => { + this.log(`Debug server listening on port ${port}\n`); + resolve(); + }); + + this.debugServer.on("error", reject); + }); + } + + // Set up message handling for debug protocol + setupDebugConnection() { + this.debugConnection.on("data", (_data) => { + // TODO + }); + + this.debugConnection.on("end", () => { + this.connected = false; + }); + } + + // Helper to send output to debug console + log(text, category = "stdout") { + this.sendEvent(new OutputEvent(text, category)); + } + + disconnectRequest(response, _args) { + this.cleanup(); + this.sendResponse(response); + } + + // Clean up resources + cleanup() { + if (this.debugConnection) { + this.debugConnection.end(); + this.debugConnection = null; + } + + if (this.debugServer) { + this.debugServer.close(); + this.debugServer = null; + } + + this.connected = false; + } +} + +module.exports = {MongoShellDebugSession};