diff --git a/.github/actions/idf/entrypoint.sh b/.github/actions/idf/entrypoint.sh index afb188f14..84a59870c 100644 --- a/.github/actions/idf/entrypoint.sh +++ b/.github/actions/idf/entrypoint.sh @@ -19,6 +19,9 @@ export PY_PKGS=$(python -m pip list --format json) yarn yarn lint Xvfb -ac :99 -screen 0 1920x1080x16 & sleep 2 & yarn test -echo ::set-output name=result::$(cat ./out/results/test-results.xml) +EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) +echo "result<<$EOF" >> $GITHUB_OUTPUT +echo "$(cat ./out/results/test-results.xml)" >> $GITHUB_OUTPUT +echo "$EOF" >> $GITHUB_OUTPUT rm -r .vscode-test \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98292f6bb..ab606f536 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - - name: Clone Repository - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: "recursive" - - - name: Setup Node.js 14 - uses: actions/setup-node@v2 + - uses: actions/setup-node@v4 with: - node-version: "14" + node-version: 18 - name: Install Node Dependencies run: yarn diff --git a/.github/workflows/ui-test.yml b/.github/workflows/ui-test.yml index c3104556a..09f77dfb6 100644 --- a/.github/workflows/ui-test.yml +++ b/.github/workflows/ui-test.yml @@ -13,8 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Clone Repository - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: "recursive" diff --git a/README.md b/README.md index 3f06b928a..decca4ac7 100644 --- a/README.md +++ b/README.md @@ -185,15 +185,15 @@ Click F1 to show Visual studio code actions, then type **ESP-IDF** to We have implemented some utilities commands that can be used in tasks.json and launch.json that can be used like: ```json -"miDebuggerPath": "${command:espIdf.getXtensaGdb}" +"miDebuggerPath": "${command:espIdf.getToolchainGdb}" ``` - `espIdf.getExtensionPath`: Get the installed location absolute path. - `espIdf.getOpenOcdScriptValue`: Return the value of OPENOCD_SCRIPTS from `idf.customExtraVars` or from system OPENOCD_SCRIPTS environment variable. - `espIdf.getOpenOcdConfig`: Return the openOCD configuration files as string. Example `-f interface/ftdi/esp32_devkitj_v1.cfg -f board/esp32-wrover.cfg`. - `espIdf.getProjectName`: Return the project name from current workspace folder `build/project_description.json`. -- `espIdf.getXtensaGcc`: Return the absolute path of the toolchain gcc for the ESP-IDF target given by `idf.adapterTargetName` configuration setting and `idf.customExtraPaths`. -- `espIdf.getXtensaGdb`: Return the absolute path of the toolchain gdb for the ESP-IDF target given by `idf.adapterTargetName` configuration setting and `idf.customExtraPaths`. +- `espIdf.getToolchainGcc`: Return the absolute path of the toolchain gcc for the ESP-IDF target given by `idf.adapterTargetName` configuration setting and `idf.customExtraPaths`. +- `espIdf.getToolchainGdb`: Return the absolute path of the toolchain gdb for the ESP-IDF target given by `idf.adapterTargetName` configuration setting and `idf.customExtraPaths`. See an example in the [debugging](./docs/DEBUGGING.md) documentation. diff --git a/docs/DEBUGGING.md b/docs/DEBUGGING.md index dfb8e9c99..c1f21821b 100644 --- a/docs/DEBUGGING.md +++ b/docs/DEBUGGING.md @@ -7,13 +7,168 @@ The Visual Studio Code uses `.vscode/launch.json` to configure debug as specified in [Visual Studio Code Debugging](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations). -We recommend using our ESP-IDF Debug Adapter to debug your ESP-IDF projects, but you can also just configure launch.json for the [Microsoft C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools). +We recommend using our `Eclipse CDT GDB Adapter` configuration to debug your ESP-IDF projects, but you can configure launch.json for any GDB debugger extension like [Microsoft C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) and [Native Debug](https://marketplace.visualstudio.com/items?itemName=webfreak.debug). The ESP-IDF Debug adapter will be deprecated and removed in the next major release. Our extension implements a `ESP-IDF: Peripheral View` tree view in the `Run and Debug` view which will use the SVD file defined in the `IDF SVD File Path (idf.svdFilePath)` configuration setting to be defined in the [settings.json](../SETTINGS.md) to populate a set of peripherals registers values for the active debug session target. You could find Espressif SVD files from [Espressif SVD](https://github.com/espressif/svd). +If `initCommands`, `gdbinitFile` or `initGdbCommands` are defined in launch.json, make sure to include the following commands for debug session to properly work as shown in [JTAG Debugging with command line](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/jtag-debugging/using-debugger.html#command-line). + +## Using the Eclipse CDT GDB Debug Adapter + +The Eclipse CDT team have published a GDB debug adapter as NPM package which we include in our extension dependencies. For more information about the debug adapter please review [CDT-GDB-Adapter Github Repository](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter). + +The default configuration is: + +```JSON +{ + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + } + ] +} +``` + +where required of the arguments are automatically defined and resolved by the extension itself. + +In case the user wants more customized control, the basic arguments in launch.json are: + +```JSON +{ + "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT Remote", + "program": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf", + "initCommands": [ + "set remote hardware-watchpoint-limit {IDF_TARGET_CPU_WATCHPOINT_NUM}", + "mon reset halt", + "maintenance flush register-cache", + "thb app_main" + ], + "gdb": "${command:espIdf.getToolchainGdb}", + "target": { + "connectCommands": [ + "set remotetimeout 20", + "-target-select extended-remote localhost:3333" + ] + } + } + ] +} +``` + +- `program`: ELF file of your project build directory to execute the debug session. The command `${command:espIdf.getProjectName}` will query the extension to find the current build directory project name. +- `initCommands`: GDB Commands to initialize GDB and target. +- `gdb`: GDB executable to be used. By default `"${command:espIdf.getToolchainGdb}"` will query the extension to find the ESP-IDF toolchain GDB for the current `IDF_TARGET` of your esp-idf project (esp32, esp32c6, etc.). + +> **NOTE** `{IDF_TARGET_CPU_WATCHPOINT_NUM}` is resolved by the extension according to the current `IDF_TARGET` of your esp-idf project (esp32, esp32c6, etc.). + +Some additional arguments you might use are: + +- `runOpenOCD`: (Default: true). Run extension openOCD Server. +- `verifyAppBinBeforeDebug`: (Default: false) Verify that current ESP-IDF project binary is the same as binary in chip. +- `logFile`: Absolute path to the file to log interaction with gdb. Example: `${workspaceFolder}/gdb.log`. +- `verbose`: Produce verbose log output. +- `environment`: Environment variables to apply to the ESP-IDF Debug Adapter. It will replace global environment variables and environment variables used by the extension. + +```json +"environment": { + "VAR": "Value" +} +``` + +- `imageAndSymbols`: + +```json +"imageAndSymbols": { + "symbolFileName": "If specified, a symbol file to load at the given (optional) offset", + "symbolOffset": "If symbolFileName is specified, the offset used to load", + "imageFileName": "If specified, an image file to load at the given (optional) offset", + "imageOffset": "If imageFileName is specified, the offset used to load" +} +``` + +- `target`: Configuration for target to be attached. Specifies how to connect to the device to debug. Usually OpenOCD exposes the chip as a remote target on port `3333`. + +```json +"target": { + "type": "The kind of target debugging to do. This is passed to -target-select (defaults to remote)", + "host": "Target host to connect to (defaults to 'localhost', ignored if parameters is set)", + "port": "Target port to connect to (defaults to value captured by serverPortRegExp, ignored if parameters is set)", + "parameters": "Target parameters for the type of target. Normally something like localhost:12345. (defaults to `${host}:${port}`)", + "connectCommands": "Replace all previous parameters to specify an array of commands to establish connection" +} +``` + +Other arguments please review this extension's package.json `gdbtarget` debugger contribution. + +## Use Microsoft C/C++ Extension to Debug + +The user can also use [Microsoft C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) to debug, the community recommend this launch.json configuration: + +```JSON +{ + "configurations": [ + { + "name": "GDB", + "type": "cppdbg", + "request": "launch", + "MIMode": "gdb", + "miDebuggerPath": "${command:espIdf.getToolchainGdb}", + "program": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf", + "windows": { + "program": "${workspaceFolder}\\build\\${command:espIdf.getProjectName}.elf" + }, + "cwd": "${workspaceFolder}", + "environment": [{ "name": "PATH", "value": "${config:idf.customExtraPaths}" }], + "setupCommands": [ + { "text": "set remotetimeout 20" }, + ], + "postRemoteConnectCommands": [ + { "text": "mon reset halt" }, + { "text": "maintenance flush register-cache"}, + ], + "externalConsole": false, + "logging": { + "engineLogging": true + } + } + ] +} +``` + +# Using NativeDebug + +The user can also try using the [Native Debug](https://marketplace.visualstudio.com/items?itemName=webfreak.debug) extension with this example launch.json configuration: + +```JSON +{ + "configurations": [ + { + "type": "gdb", + "request": "attach", + "name": "NativeDebug", + "target": "extended-remote :3333", + "executable": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf", + "gdbpath": "${command:espIdf.getToolchainGdb}", + "cwd": "${workspaceRoot}", + "autorun": [ + "mon reset halt", + "maintenance flush register-cache", + "thb app_main" + ] + } + ] +} +``` + ## Use the ESP-IDF Debug Adapter -> **NOTE:** Currently the python package `pygdbmi` used by the debug adapter still depends on some Python 2.7 libraries (libpython2.7.so.1.0) so make sure that the Python executable you use in `idf.pythonBinPath` contains these libraries. This will be dropped in later versions of ESP-IDF. +**DEPRECATED NOTICE**: We are deprecating the use of our ESP-IDF Debug Adapter in favor of using the Eclipse CDT GDB Adapter. It will removed from extension in the future major release. This extension includes the [ESP-IDF Debug Adapter](https://github.com/espressif/esp-debug-adapter) which implement the debug adapter protocol (DAP) to communicate Xtensa's Toolchain and OpenOCD with Visual Studio Code allowing the user to easily debug ESP-IDF applications. Visual Studio Code will: @@ -45,7 +200,7 @@ The ESP-IDF Debug Adapter settings for launch.json are: - `initGdbCommands`: One or more xtensa-esp32-elf-gdb commands to execute in order to setup the underlying debugger. > **NOTE**: If `gdbinitFile` is defined, these commands will be ignored. - `logLevel`: Debug Adapter logging level (0-4), 5 - for a full OOCD log. Default: 2. -- `mode`: Can be either `auto`, to start the Debug Adapter and OpenOCD server within the extension or `manual`, to connect to existing Debug Adapter and OpenOCD session. Default: auto. +- `mode`: Can be either `auto`, to start the Debug Adapter and OpenOCD server within the extension or `manual`, to connect to an already running Debug Adapter and OpenOCD session. Default: auto. > **NOTE:** If set to `manual`, OpenOCD and ESP-IDF Debug Adapter have to be manually executed by the user and the extension will just try to connect to existing servers at configured ports. - `name`: The name of the debug launch configuration. This will be shown in the Run view (Menu View -> Run). - `type`: Type of debug configuration. It **must** be `espidf`. @@ -67,7 +222,6 @@ Example launch.json for ESP-IDF Debug Adapter: ```JSON { - "version": "0.2.0", "configurations": [ { "type": "espidf", @@ -80,9 +234,10 @@ Example launch.json for ESP-IDF Debug Adapter: "tmoScaleFactor": 1, "initGdbCommands": [ "target remote :3333", + "set remotetimeout 20", "symbol-file /path/to/program.elf", "mon reset halt", - "flushregs", + "maintenance flush register-cache", "thb app_main" ], "env": { @@ -95,40 +250,4 @@ Example launch.json for ESP-IDF Debug Adapter: ### Output and Logs from ESP-IDF Debug Adapter and OpenOCD -Beside the Visual Studio Code Debug console output. You can find the debug adapter output in `/debug.log` and Menu View -> Output -> `ESP-IDF Debug Adapter` as well as OpenOCD output in Menu View -> Output -> `OpenOCD`. - -## Use Microsoft C/C++ Extension to Debug - -The user can also use [Microsoft C/C++ Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) to debug, the community recommend this launch.json configuration: - -```JSON -{ - "version": "0.2.0", - "configurations": [ - { - "name": "GDB", - "type": "cppdbg", - "request": "launch", - "MIMode": "gdb", - "miDebuggerPath": "${command:espIdf.getXtensaGdb}", - "program": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf", - "windows": { - "program": "${workspaceFolder}\\build\\${command:espIdf.getProjectName}.elf" - }, - "cwd": "${workspaceFolder}", - "environment": [{ "name": "PATH", "value": "${config:idf.customExtraPaths}" }], - "setupCommands": [ - { "text": "target remote :3333" }, - { "text": "set remote hardware-watchpoint-limit 2"}, - { "text": "mon reset halt" }, - { "text": "thb app_main" }, - { "text": "flushregs" } - ], - "externalConsole": false, - "logging": { - "engineLogging": true - } - } - ] -} -``` +Beside the Visual Studio Code Debug console output. You can find OpenOCD and the ESP-IDF debug adapter output in `/debug.log` and Menu View -> Output -> `ESP-IDF`. diff --git a/docs/tutorial/debugging.md b/docs/tutorial/debugging.md index e78feda01..506267768 100644 --- a/docs/tutorial/debugging.md +++ b/docs/tutorial/debugging.md @@ -4,19 +4,8 @@ This tutorial shows the user how to debug ESP-IDF projects using the Visual Studio Code extension for ESP-IDF. If you haven't configured the extension as explained in [Install tutorial](./install.md) please do it first. -> **NOTE:** If there is any Python package error, please try to reinstall the required python packages with the **ESP-IDF: Install ESP-IDF Python Packages** command. - -> **NOTE:** Currently the python package `pygdbmi` used by the debug adapter still depends on some Python 2.7 libraries (libpython2.7.so.1.0) so make sure that the Python executable in `idf.pythonBinPath` you use contains these libraries. This will be dropped in later versions of ESP-IDF. - 1. Configure, build and flash your project as explained in [Basic use tutorial](./basic_use.md). -2. Set the proper values for openOCD Configuration files in the `idf.openOCDConfigs` configuration setting. You can choose a specific board listed in openOCD using **ESP-IDF: Select OpenOCD Board Configuration** or use **ESP-IDF: Device Configuration** to manually set any value you desire. - -When you use **ESP-IDF: Set Espressif Device Target** the following files are set: - -- Choosing esp32 as IDF_TARGET will set `idf.openOCDConfigs` to ["interface/ftdi/esp32_devkitj_v1.cfg", "target/esp32.cfg"] -- Choosing esp32s2 as IDF_TARGET will set `idf.openOCDConfigs` to ["interface/ftdi/esp32_devkitj_v1.cfg", "target/esp32s2.cfg"] -- Choosing esp32s3 as IDF_TARGET will set `idf.openOCDConfigs` to ["interface/ftdi/esp32_devkitj_v1.cfg", "target/esp32s3.cfg"] -- Choosing esp32c3 as IDF_TARGET will set `idf.openOCDConfigs` to ["board/esp32c3-builtin.cfg"] if using built-in usb jtag or ["board/esp32c3-ftdi.cfg"] if using ESP-PROG-JTAG. +2. Set the proper values for openOCD Configuration files in the `idf.openOCDConfigs` configuration setting. You can choose a specific board listed in openOCD using **ESP-IDF: Select OpenOCD Board Configuration**, **ESP-IDF: Set Espressif Device Target** or use **ESP-IDF: Device Configuration** to manually set any value you desire. > **NOTE:** Please take a look at [Configuring of OpenOCD for specific target](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/jtag-debugging/tips-and-quirks.html#configuration-of-openocd-for-specific-target) for more information about these configuration files. @@ -24,13 +13,13 @@ When you use **ESP-IDF: Set Espressif Device Target** the following files are se Several steps will be automatically done for you but explained for clarity. You can skip to step 6 to continue the debug tutorial part. -4. OpenOCD server is launched in the background and the output is shown in menu `View` -> Output -> OpenOCD. By default it will be launched using localhost, port 4444 for Telnet communication, port 6666 for TCL communication and port 3333 for gdb. +4. OpenOCD server is launched in the background and the output is shown in menu `View` -> Output -> ESP-IDF. By default it will be launched using localhost, port `4444` for Telnet communication, port `6666` for TCL communication and port `3333` for gdb. -> **NOTE:** The user can start or stop the openOCD from Visual Studio Code using the **ESP-IDF: OpenOCD Manager** command or from the `OpenOCD Server (Running | Stopped)` button in the visual studio code status bar. +> **NOTE:** The user can start or stop the OpenOCD from Visual Studio Code using the **ESP-IDF: OpenOCD Manager** command or from the `OpenOCD Server (Running | Stopped)` button in the visual studio code status bar. > **NOTE:** The user can modify `openocd.tcl.host` and `openocd.tcl.port` configuration settings to modify these values. You can also set `idf.openOcdDebugLevel` to lower or increase (0-4) the messages from OpenOCD in the OpenOCD output. Please review [ESP-IDF Settings](../SETTINGS.md) to see how to modify these configuration settings. -5. The [ESP-IDF Debug Adapter](https://github.com/espressif/esp-debug-adapter) server is launched in the background and the output is shown in menu View -> Output -> `ESP-IDF Debug Adapter`. This server is a proxy between Visual Studio Code, configured toolchain GDB and OpenOCD server. It will be launched at port `43474` by default. Please review [Debugging](../DEBUGGING.md) for more information how to customize the debugging behavior like application offset, logging level and set your own gdb startup commands. +5. The [Eclipse CDT GDB Adapter](https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter) is launched in the background and the output is shown in the Debug Console. This adapter is a proxy between Visual Studio Code, configured toolchain GDB and OpenOCD server. Please review [Debugging](../DEBUGGING.md) for more information how to customize the debugging behavior by modifying launch.json arguments. 6. The debug session will start after the debug adapter server is launched and ready. @@ -40,9 +29,9 @@ Several steps will be automatically done for you but explained for clarity. You # Navigating Through the Code, Call Stack and Threads -7. When the target is halted, the editor will show the line of code where the program halted and the list of threads in the `Call Stack` sub-window on the `Run` icon in the Activity Bar on the side of Visual Studio Code. The first line of call stack under Thread #1 contains the last called function `app_main()`, which in turned was called from `main_task()` as shown in the previous image. Each line of the stack also contains the file name and line number where the function was called. By clicking on each of the stack entries, you will see the file opened. +7. When the target is halted, the editor will show the line of code where the program halted and the list of threads in the `Call Stack` sub-window `(a)` on the `Run` icon in the Activity Bar on the side of Visual Studio Code. The first line of call stack under main `(b)` contains the last called function `app_main()`, which in turned was called from `main_task()` as shown in the previous image. Each line of the stack also contains the file name and line number `(c)` where the function was called. By clicking on each of the stack entries, you will see the file opened. -By expanding threads you can navigate throughout the application. Thread #5 that contains much longer call stack where the user can see, besides function calls, numbers like `0x4000bff0` representing address of binary code not provided in source form. +By expanding threads you can navigate throughout the application. Some threads contains much longer call stack where the user can see, besides function calls, numbers like `0x4000bff0` representing address of binary code not provided in source form.

Threads @@ -135,15 +124,11 @@ You can check the assembly code from the debugging session by doing a right clic # Watchpoints (Data Breakpoints) -You can set breakpoint on variable read, change or access by right clicking the variable in the debug session Variables view and click on `Break on Value Read`, `Break on Value Write` and `Break on Value Change`. See [ESP-IDF breakpoints and watchpoints available](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/jtag-debugging/tips-and-quirks.html#breakpoints-and-watchpoints-available) for more information. - -

- Break on value -

+See [ESP-IDF breakpoints and watchpoints available](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/jtag-debugging/tips-and-quirks.html#breakpoints-and-watchpoints-available) for more information. # Next steps -You can send any GDB commands in the Debug console with `--exec COMMAND`. You need to set `logLevel: 5` in the project's launch.json to see the command output. +You can send any GDB commands in the Debug console with `> COMMAND`. For example `> i threads`.

GDB Commands @@ -151,13 +136,17 @@ You can send any GDB commands in the Debug console with `--exec COMMAND`. You ne More about [Command Line Debugging](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/jtag-debugging/debugging-examples.html#command-line). -Our extension implements a `ESP-IDF: Peripheral View` tree view in the `Run and Debug` view which will use the SVD file defined in the `IDF Svd File Path (idf.svdFilePath)` configuration setting in [settings.json](../SETTINGS.md) to populate a set of peripherals registers values for the active debug session target. You could find Espressif SVD files from [Espressif SVD](https://github.com/espressif/svd). +Our extension implements a `ESP-IDF: Peripheral View` tree view in the `Run and Debug` view which will use the SVD file defined in the `IDF Svd File Path (idf.svdFilePath)` configuration setting in [settings.json](../SETTINGS.md) to populate a set of peripherals registers values for the active debug session target. You could download Espressif SVD files from [Espressif SVD](https://github.com/espressif/svd) repository. + +

+ GDB Commands +

You can start a monitor session that can capture fatal error events with `ESP-IDF: Launch IDF Monitor for CoreDump / GDB-Stub Mode` command and, if configured in your project's sdkconfig, trigger the start of a debug session for GDB remote protocol server (GDBStub) or [ESP-IDF Core Dump](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/core_dump.html#core-dump) when an error is found. Read more in the [panic handler documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/fatal-errors.html#panic-handler). - **Core Dump** is configured when `Core Dump's Data Destination` is set to either `UART` or `FLASH` using the `ESP-IDF: SDK Configuration Editor` extension command or `idf.py menuconfig` in a terminal. - **GDB Stub** is configured when `Panic Handler Behaviour` is set to `Invoke GDBStub` using the`ESP-IDF: SDK Configuration Editor` extension command or `idf.py menuconfig` in a terminal. -The user can modify the debug session as shown in the [Debugging](../DEBUGGING.md) documentation by customizing settings such as the program start address offset, the ESP-IDF Debug Adapter server port, logging level and custom initial gdb commands. +The user can modify the debug session as shown in the [Debugging](../DEBUGGING.md) documentation by customizing launch.json arguments such as custom initial gdb commands. See other [ESP-IDF extension features](../FEATURES.md). diff --git a/media/tutorials/debug/break_on_variable.png b/media/tutorials/debug/break_on_variable.png deleted file mode 100644 index 9641ab790..000000000 Binary files a/media/tutorials/debug/break_on_variable.png and /dev/null differ diff --git a/media/tutorials/debug/breakpoint.png b/media/tutorials/debug/breakpoint.png index f07e30caf..c833f2734 100644 Binary files a/media/tutorials/debug/breakpoint.png and b/media/tutorials/debug/breakpoint.png differ diff --git a/media/tutorials/debug/conditional_breakpoint.png b/media/tutorials/debug/conditional_breakpoint.png index f2e7552f5..0fd3bd126 100644 Binary files a/media/tutorials/debug/conditional_breakpoint.png and b/media/tutorials/debug/conditional_breakpoint.png differ diff --git a/media/tutorials/debug/disassembly_view.png b/media/tutorials/debug/disassembly_view.png index 01582d014..e5c109155 100644 Binary files a/media/tutorials/debug/disassembly_view.png and b/media/tutorials/debug/disassembly_view.png differ diff --git a/media/tutorials/debug/gdb_commands.png b/media/tutorials/debug/gdb_commands.png index a925e3342..dee12471f 100644 Binary files a/media/tutorials/debug/gdb_commands.png and b/media/tutorials/debug/gdb_commands.png differ diff --git a/media/tutorials/debug/init_halted.png b/media/tutorials/debug/init_halted.png index 5c9fa0e94..acb778c71 100644 Binary files a/media/tutorials/debug/init_halted.png and b/media/tutorials/debug/init_halted.png differ diff --git a/media/tutorials/debug/peripheral_viewer.png b/media/tutorials/debug/peripheral_viewer.png new file mode 100644 index 000000000..0db197620 Binary files /dev/null and b/media/tutorials/debug/peripheral_viewer.png differ diff --git a/media/tutorials/debug/step_into.png b/media/tutorials/debug/step_into.png index f6ebe09b1..f5bedc04b 100644 Binary files a/media/tutorials/debug/step_into.png and b/media/tutorials/debug/step_into.png differ diff --git a/media/tutorials/debug/step_out.png b/media/tutorials/debug/step_out.png index 532011d32..6ede6e8a7 100644 Binary files a/media/tutorials/debug/step_out.png and b/media/tutorials/debug/step_out.png differ diff --git a/media/tutorials/debug/step_over.png b/media/tutorials/debug/step_over.png index e38432d25..667bf4bc9 100644 Binary files a/media/tutorials/debug/step_over.png and b/media/tutorials/debug/step_over.png differ diff --git a/media/tutorials/debug/thread5.png b/media/tutorials/debug/thread5.png index 5cf9a1e88..9845e91ce 100644 Binary files a/media/tutorials/debug/thread5.png and b/media/tutorials/debug/thread5.png differ diff --git a/media/tutorials/debug/watch_set_program_vars.png b/media/tutorials/debug/watch_set_program_vars.png index de9789f3a..765245ade 100644 Binary files a/media/tutorials/debug/watch_set_program_vars.png and b/media/tutorials/debug/watch_set_program_vars.png differ diff --git a/package.json b/package.json index 832573b60..97f652df6 100644 --- a/package.json +++ b/package.json @@ -1399,7 +1399,8 @@ "type": "espidf", "label": "ESP-IDF", "languages": [ - "cpp" + "cpp", + "c" ], "runtime": "python", "configurationAttributes": { @@ -1483,6 +1484,516 @@ "description": "%debug.initConfig.description%" } ] + }, + { + "type": "gdbtarget", + "label": "Eclipse GDB Target", + "runtime": "node", + "configurationAttributes": { + "launch": { + "properties": { + "gdb": { + "type": "string", + "description": "Path to gdb", + "default": "${command:espIdf.getToolchainGdb}" + }, + "cwd": { + "type": "string", + "description": "Working directory (cwd) to use when launching gdb. Defaults to the directory of the 'program'" + }, + "runOpenOCD": { + "type": "boolean", + "description": "Run OpenOCD Server", + "default": true + }, + "environment": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "default": {}, + "description": "Environment variables to use when launching gdb, defined as a key-value pairs. Use null value to remove variable. For example:\n\"environment\": {\n \"VARNAME\": \"value\",\n \"PATH\": \"/new/item:${env:PATH}\",\n \"REMOVEME\": null\n}", + "type": "object" + }, + "program": { + "type": "string", + "description": "Path to the program to be launched", + "default": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf" + }, + "gdbAsync": { + "type": "boolean", + "description": "Use mi-async mode for communication with GDB. (defaults to true, always true when gdbNonStop is true)", + "default": true + }, + "gdbNonStop": { + "type": "boolean", + "description": "Use non-stop mode for controlling multiple threads. (defaults to false)", + "default": false + }, + "verbose": { + "type": "boolean", + "description": "Produce verbose log output", + "default": false + }, + "verifyAppBinBeforeDebug": { + "type": "boolean", + "description": "%esp_idf.verifyAppBinBeforeDebug.description%", + "default": false + }, + "logFile": { + "type": "string", + "description": "Absolute path to the file to log interaction with gdb" + }, + "openGdbConsole": { + "type": "boolean", + "description": "(UNIX-only) Open a GDB console in your IDE while debugging" + }, + "initCommands": { + "type": "array", + "description": "List of GDB commands sent after attaching to target before loading image on target.", + "items": { + "type": "string" + }, + "default": [] + }, + "preRunCommands": { + "type": "array", + "description": "List of GDB commands sent after loading image on target before resuming target.", + "items": { + "type": "string" + }, + "default": [] + }, + "imageAndSymbols": { + "type": "object", + "default": {}, + "properties": { + "symbolFileName": { + "type": "string", + "description": "If specified, a symbol file to load at the given (optional) offset", + "default": "" + }, + "symbolOffset": { + "type": "string", + "description": "If symbolFileName is specified, the offset used to load", + "default": "" + }, + "imageFileName": { + "type": "string", + "description": "If specified, an image file to load at the given (optional) offset", + "default": "" + }, + "imageOffset": { + "type": "string", + "description": "If imageFileName is specified, the offset used to load", + "default": "" + } + } + }, + "target": { + "type": "object", + "default": { + "connectCommands": [ + "set remotetimeout 20", + "-target-select extended-remote localhost:3333" + ] + }, + "properties": { + "type": { + "type": "string", + "description": "The kind of target debugging to do. This is passed to -target-select (defaults to remote)", + "default": "remote" + }, + "parameters": { + "type": "array", + "description": "Target parameters for the type of target. Normally something like localhost:12345. (defaults to `${host}:${port}`)", + "items": { + "type": "string" + }, + "default": [] + }, + "connectCommands": { + "type": "array", + "description": "Commands to execute for target connection. Normally something like [`-target-select extended-remote localhost:3333`]", + "items": { + "type": "string" + }, + "default": [ + "set remotetimeout 20", + "-target-select extended-remote localhost:3333" + ] + }, + "host": { + "type": "string", + "description": "Target host to connect to (defaults to 'localhost', ignored if parameters is set)", + "default": "localhost" + }, + "port": { + "type": "string", + "description": "Target port to connect to (defaults to value captured by serverPortRegExp, ignored if parameters is set)", + "default": "2331" + }, + "cwd": { + "type": "string", + "description": "Specifies the working directory of server (defaults to the working directory of gdb)", + "default": "" + }, + "environment": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "default": {}, + "description": "Environment variables to use when launching server (defaults to the environment used to launch gdb), defined as a key-value pairs. Use null value to remove variable. For example:\n\"environment\": {\n \"VARNAME\": \"value\",\n \"PATH\": \"/new/item:${env:PATH}\",\n \"REMOVEME\": null\n}", + "type": "object" + }, + "server": { + "type": "string", + "description": "The executable for the target server to launch (e.g. gdbserver or JLinkGDBServerCLExe) (defaults to gdbserver)", + "default": "gdbserver" + }, + "serverParameters": { + "type": "array", + "description": "Command line arguments passed to server (defaults to --once :0 ${args.program}).", + "items": { + "type": "string" + }, + "default": [] + }, + "serverPortRegExp": { + "type": "string", + "description": "Regular expression to extract port from by examinging stdout/err of server. Once server is launched, port will be set to this if port is not set. Defaults to matching a string like 'Listening on port 41551' which is what gdbserver provides. Ignored if port or parameters is set", + "default": "" + }, + "serverStartupDelay": { + "type": "number", + "description": "Delay after startup before continuing launch, in milliseconds. If serverPortRegExp is provided, it is the delay after that regexp is seen.", + "default": "0" + }, + "automaticallyKillServer": { + "type": "boolean", + "description": "Automatically kill the launched server when client issues a disconnect (default: true)" + }, + "uart": { + "type": "object", + "description": "Settings related to displaying UART output in the debug console", + "properties": { + "serialPort": { + "type": "string", + "description": "Path to the serial port connected to the UART on the board." + }, + "socketPort": { + "type": "string", + "description": "Target TCP port on the host machine to attach socket to print UART output (defaults to 3456)", + "default": "3456" + }, + "baudRate": { + "type": "number", + "description": "Baud Rate (in bits/s) of the serial port to be opened (defaults to 115200).", + "default": 115200 + }, + "characterSize": { + "type": "number", + "enum": [ + 5, + 6, + 7, + 8 + ], + "description": "The number of bits in each character of data sent across the serial line (defaults to 8).", + "default": 8 + }, + "parity": { + "type": "string", + "enum": [ + "none", + "odd", + "even", + "mark", + "space" + ], + "description": "The type of parity check enabled with the transmitted data (defaults to \"none\" - no parity bit sent)", + "default": "none" + }, + "stopBits": { + "type": "number", + "enum": [ + 1, + 1.5, + 2 + ], + "description": "The number of stop bits sent to allow the receiver to detect the end of characters and resynchronize with the character stream (defaults to 1).", + "default": 1 + }, + "handshakingMethod": { + "type": "string", + "enum": [ + "none", + "XON/XOFF", + "RTS/CTS" + ], + "description": "The handshaking method used for flow control across the serial line (defaults to \"none\" - no handshaking)", + "default": "none" + }, + "eolCharacter": { + "type": "string", + "enum": [ + "LF", + "CRLF" + ], + "description": "The EOL character used to parse the UART output line-by-line (defaults to \"LF\").", + "default": "LF" + } + } + } + } + } + } + }, + "attach": { + "properties": { + "gdb": { + "type": "string", + "description": "Path to gdb", + "default": "${command:espIdf.getToolchainGdb}" + }, + "cwd": { + "type": "string", + "description": "Working directory (cwd) to use when launching gdb. Defaults to the directory of the 'program'" + }, + "runOpenOCD": { + "type": "boolean", + "description": "Run OpenOCD Server", + "default": true + }, + "environment": { + "additionalProperties": { + "type": [ + "string", + "null" + ] + }, + "default": {}, + "description": "Environment variables to use when launching gdb, defined as a key-value pairs. Use null value to remove variable. For example:\n\"environment\": {\n \"VARNAME\": \"value\",\n \"PATH\": \"/new/item:${env:PATH}\",\n \"REMOVEME\": null\n}", + "type": "object" + }, + "program": { + "type": "string", + "description": "Path to the program to be debugged", + "default": "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf" + }, + "gdbAsync": { + "type": "boolean", + "description": "Use mi-async mode for communication with GDB. (defaults to true, always true when gdbNonStop is true)", + "default": true + }, + "gdbNonStop": { + "type": "boolean", + "description": "Use non-stop mode for controlling multiple threads. (defaults to false)", + "default": false + }, + "verbose": { + "type": "boolean", + "description": "Produce verbose log output", + "default": false + }, + "verifyAppBinBeforeDebug": { + "type": "boolean", + "description": "%esp_idf.verifyAppBinBeforeDebug.description%", + "default": false + }, + "logFile": { + "type": "string", + "description": "Absolute path to the file to log interaction with gdb" + }, + "openGdbConsole": { + "type": "boolean", + "description": "(UNIX-only) Open a GDB console in your IDE while debugging" + }, + "initCommands": { + "type": "array", + "description": "List of GDB commands sent after attaching to target before loading image on target.", + "items": { + "type": "string" + }, + "default": [] + }, + "preRunCommands": { + "type": "array", + "description": "List of GDB commands sent after loading image on target before resuming target.", + "items": { + "type": "string" + }, + "default": [] + }, + "imageAndSymbols": { + "type": "object", + "default": {}, + "properties": { + "symbolFileName": { + "type": "string", + "description": "If specified, a symbol file to load at the given (optional) offset", + "default": "" + }, + "symbolOffset": { + "type": "string", + "description": "If symbolFileName is specified, the offset used to load", + "default": "" + }, + "imageFileName": { + "type": "string", + "description": "If specified, an image file to load at the given (optional) offset", + "default": "" + }, + "imageOffset": { + "type": "string", + "description": "If imageFileName is specified, the offset used to load", + "default": "" + } + } + }, + "target": { + "type": "object", + "default": { + "connectCommands": [ + "set remotetimeout 20", + "-target-select extended-remote localhost:3333" + ] + }, + "properties": { + "type": { + "type": "string", + "description": "The kind of target debugging to do. This is passed to -target-select (defaults to remote)", + "default": "remote" + }, + "parameters": { + "type": "array", + "description": "Target parameters for the type of target. Normally something like localhost:12345. (defaults to `${host}:${port}`)", + "items": { + "type": "string" + }, + "default": [] + }, + "host": { + "type": "string", + "description": "Target host to connect to (defaults to 'localhost', ignored if parameters is set)", + "default": "localhost" + }, + "port": { + "type": "string", + "description": "Target port to connect to (defaults to value captured by serverPortRegExp, ignored if parameters is set)", + "default": "2331" + }, + "uart": { + "type": "object", + "description": "Settings related to displaying UART output in the debug console", + "properties": { + "serialPort": { + "type": "string", + "description": "Path to the serial port connected to the UART on the board." + }, + "socketPort": { + "type": "string", + "description": "Target TCP port on the host machine to attach socket to print UART output (defaults to 3456)", + "default": "3456" + }, + "baudRate": { + "type": "number", + "description": "Baud Rate (in bits/s) of the serial port to be opened (defaults to 115200).", + "default": 115200 + }, + "characterSize": { + "type": "number", + "enum": [ + 5, + 6, + 7, + 8 + ], + "description": "The number of bits in each character of data sent across the serial line (defaults to 8).", + "default": 8 + }, + "parity": { + "type": "string", + "enum": [ + "none", + "odd", + "even", + "mark", + "space" + ], + "description": "The type of parity check enabled with the transmitted data (defaults to \"none\" - no parity bit sent)", + "default": "none" + }, + "stopBits": { + "type": "number", + "enum": [ + 1, + 1.5, + 2 + ], + "description": "The number of stop bits sent to allow the receiver to detect the end of characters and resynchronize with the character stream (defaults to 1).", + "default": 1 + }, + "handshakingMethod": { + "type": "string", + "enum": [ + "none", + "XON/XOFF", + "RTS/CTS" + ], + "description": "The handshaking method used for flow control across the serial line (defaults to \"none\" - no handshaking)", + "default": "none" + }, + "eolCharacter": { + "type": "string", + "enum": [ + "LF", + "CRLF" + ], + "description": "The EOL character used to parse the UART output line-by-line (defaults to \"LF\").", + "default": "LF" + } + } + }, + "connectCommands": { + "type": "array", + "description": "Commands to execute for target connection. Normally something like [`-target-select extended-remote localhost:3333`]", + "items": { + "type": "string" + }, + "default": [ + "set remotetimeout 20", + "-target-select extended-remote localhost:3333" + ] + } + } + } + } + } + }, + "initialConfigurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + } + ], + "configurationSnippets": [ + { + "label": "Eclipse CDT GDB Adapter", + "description": "A espidf configuration for remote debugging using GDB.", + "body": { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + } + } + ] } ], "languages": [ @@ -1566,15 +2077,16 @@ "@types/ws": "^7.2.5", "@types/xml2js": "^0.4.11", "@types/yauzl": "^2.9.1", - "@vscode/debugadapter": "^1.53.0", + "@vscode/debugadapter": "^1.65.0", "@vscode/debugadapter-testsupport": "^1.51.0", - "@vscode/debugprotocol": "^1.53.0", + "@vscode/debugprotocol": "^1.65.0", "@vscode/extension-telemetry": "0.4.8", "@vscode/l10n-dev": "^0.0.35", "@vscode/test-electron": "^2.1.2", "@vue/compiler-sfc": "^3.3.4", "bulma": "^0.9.3", "chai": "^4.3.4", + "copy-webpack-plugin": "^12.0.2", "css-loader": "^3.1.0", "d3-scale": "^4.0.2", "file-loader": "^6.2.0", @@ -1591,6 +2103,7 @@ "reflect-metadata": "^0.1.13", "sass": "^1.49.8", "sass-loader": "^10", + "string-replace-loader": "^3.1.0", "style-loader": "^3.3.1", "ts-loader": "^9.4.4", "typescript": "^5.2.2", @@ -1625,9 +2138,11 @@ "plotly.js-dist-min": "^2.26.1", "postcss": "^8.4.31", "sanitize-html": "^2.12.1", + "serialport": "^12.0.0", "stream-browserify": "^3.0.0", "tar-fs": "^2.0.0", "tree-kill": "^1.2.2", + "utf8": "^3.0.0", "vscode-languageclient": "^5.2.1", "vscode-languageserver": "^5.2.1", "vue": "^3.3.4", diff --git a/src/cdtDebugAdapter/adapter/GDBBackend.ts b/src/cdtDebugAdapter/adapter/GDBBackend.ts new file mode 100644 index 000000000..e0bd21711 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/GDBBackend.ts @@ -0,0 +1,317 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { spawn, ChildProcess } from 'child_process'; +import * as events from 'events'; +import { Writable } from 'stream'; +import { logger } from '@vscode/debugadapter/lib/logger'; +import { + AttachRequestArguments, + LaunchRequestArguments, +} from './GDBDebugSession'; +import * as mi from './mi'; +import { MIResponse } from './mi'; +import { MIParser } from './MIParser'; +import { VarManager } from './varManager'; +import { + compareVersions, + getGdbVersion, + createEnvValues, + getGdbCwd, +} from './util'; + +export interface MIExecNextRequest { + reverse?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MIExecNextResponse extends MIResponse {} + +export interface MIGDBShowResponse extends MIResponse { + value?: string; +} + +export declare interface GDBBackend { + on( + event: 'consoleStreamOutput', + listener: (output: string, category: string) => void + ): this; + on( + event: 'execAsync' | 'notifyAsync' | 'statusAsync', + listener: (asyncClass: string, data: any) => void + ): this; + + emit( + event: 'consoleStreamOutput', + output: string, + category: string + ): boolean; + emit( + event: 'execAsync' | 'notifyAsync' | 'statusAsync', + asyncClass: string, + data: any + ): boolean; +} + +export class GDBBackend extends events.EventEmitter { + protected parser = new MIParser(this); + protected varMgr = new VarManager(this); + protected out?: Writable; + protected token = 0; + protected proc?: ChildProcess; + private gdbVersion?: string; + protected gdbAsync = false; + protected gdbNonStop = false; + protected hardwareBreakpoint = false; + + get varManager(): VarManager { + return this.varMgr; + } + + public async spawn( + requestArgs: LaunchRequestArguments | AttachRequestArguments + ) { + const gdbPath = requestArgs.gdb || 'gdb'; + this.gdbVersion = await getGdbVersion( + gdbPath, + getGdbCwd(requestArgs), + requestArgs.environment + ); + let args = ['--interpreter=mi2']; + if (requestArgs.gdbArguments) { + args = args.concat(requestArgs.gdbArguments); + } + const gdbEnvironment = requestArgs.environment + ? createEnvValues(process.env, requestArgs.environment) + : process.env; + this.proc = spawn(gdbPath, args, { + cwd: getGdbCwd(requestArgs), + env: gdbEnvironment, + }); + if (this.proc.stdin == null || this.proc.stdout == null) { + throw new Error('Spawned GDB does not have stdout or stdin'); + } + this.out = this.proc.stdin; + this.hardwareBreakpoint = requestArgs.hardwareBreakpoint ? true : false; + await this.parser.parse(this.proc.stdout); + if (this.proc.stderr) { + this.proc.stderr.on('data', (chunk) => { + const newChunk = chunk.toString(); + this.emit('consoleStreamOutput', newChunk, 'stderr'); + }); + } + await this.setNonStopMode(requestArgs.gdbNonStop); + await this.setAsyncMode(requestArgs.gdbAsync); + } + + public async setAsyncMode(isSet?: boolean) { + const command = this.gdbVersionAtLeast('7.8') + ? 'mi-async' + : 'target-async'; + if (isSet === undefined) { + isSet = true; + } + if (this.gdbNonStop) { + isSet = true; + } + const onoff = isSet ? 'on' : 'off'; + try { + await this.sendCommand(`-gdb-set ${command} ${onoff}`); + this.gdbAsync = isSet; + } catch { + // no async support - normally this only happens on Windows + // when doing host debugging. We explicitly set this + // to off here so that we get the error propogate if the -gdb-set + // failed and to make it easier to read the log + await this.sendCommand(`-gdb-set ${command} off`); + this.gdbAsync = false; + } + } + + public getAsyncMode(): boolean { + return this.gdbAsync; + } + + public async setNonStopMode(isSet?: boolean) { + if (isSet === undefined) { + isSet = false; + } + if (isSet) { + await this.sendCommand('-gdb-set pagination off'); + } + const onoff = isSet ? 'on' : 'off'; + try { + await this.sendCommand(`-gdb-set non-stop ${onoff}`); + this.gdbNonStop = isSet; + } catch { + // no non-stop support - normally this only happens on Windows. + // We explicitly set this to off here so that we get the error + // propogate if the -gdb-set failed and to make it easier to + // read the log + await this.sendCommand(`-gdb-set non-stop off`); + this.gdbNonStop = false; + } + } + + public isNonStopMode() { + return this.gdbNonStop; + } + + // getBreakpointOptions called before inserting the breakpoint and this + // method could overridden in derived classes to dynamically control the + // breakpoint insert options. If an error thrown from this method, then + // the breakpoint will not be inserted. + public async getBreakpointOptions( + _: mi.MIBreakpointLocation, + initialOptions: mi.MIBreakpointInsertOptions + ): Promise { + return initialOptions; + } + + public isUseHWBreakpoint() { + return this.hardwareBreakpoint; + } + + public pause(threadId?: number) { + if (this.gdbAsync) { + mi.sendExecInterrupt(this, threadId); + } else { + if (!this.proc) { + throw new Error('GDB is not running, nothing to interrupt'); + } + logger.verbose(`GDB signal: SIGINT to pid ${this.proc.pid}`); + this.proc.kill('SIGINT'); + } + } + + public async supportsNewUi( + gdbPath?: string, + gdbCwd?: string, + environment?: Record + ): Promise { + this.gdbVersion = await getGdbVersion( + gdbPath || 'gdb', + gdbCwd, + environment + ); + return this.gdbVersionAtLeast('7.12'); + } + + public gdbVersionAtLeast(targetVersion: string): boolean { + if (!this.gdbVersion) { + throw new Error('gdbVersion needs to be set first'); + } + return compareVersions(this.gdbVersion, targetVersion) >= 0; + } + + public async sendCommands(commands?: string[]) { + if (commands) { + for (const command of commands) { + await this.sendCommand(command); + } + } + } + + public sendCommand(command: string): Promise { + const token = this.nextToken(); + logger.verbose(`GDB command: ${token} ${command}`); + return new Promise((resolve, reject) => { + if (this.out) { + /* Set error to capture the stack where the request originated, + not the stack of reading the stream and parsing the message. + */ + const failure = new Error(); + this.parser.queueCommand(token, (resultClass, resultData) => { + switch (resultClass) { + case 'done': + case 'running': + case 'connected': + case 'exit': + resolve(resultData); + break; + case 'error': + failure.message = resultData.msg; + reject(failure); + break; + default: + failure.message = `Unknown response ${resultClass}: ${JSON.stringify( + resultData + )}`; + reject(failure); + } + }); + this.out.write(`${token}${command}\n`); + } else { + reject(new Error('gdb is not running.')); + } + }); + } + + public sendEnablePrettyPrint() { + return this.sendCommand('-enable-pretty-printing'); + } + + // Rewrite the argument escaping whitespace, quotes and backslash + public standardEscape(arg: string, needQuotes = true): string { + let result = ''; + for (const char of arg) { + if (char === '\\' || char === '"') { + result += '\\'; + } + if (char == ' ') { + needQuotes = true; + } + result += char; + } + if (needQuotes) { + result = `"${result}"`; + } + return result; + } + + public sendFileExecAndSymbols(program: string) { + return this.sendCommand( + `-file-exec-and-symbols ${this.standardEscape(program)}` + ); + } + + public sendFileSymbolFile(symbols: string) { + return this.sendCommand( + `-file-symbol-file ${this.standardEscape(symbols)}` + ); + } + + public sendAddSymbolFile(symbols: string, offset: string) { + return this.sendCommand( + `add-symbol-file ${this.standardEscape(symbols)} ${offset}` + ); + } + + public sendLoad(imageFileName: string, imageOffset: string | undefined) { + return this.sendCommand( + `load ${this.standardEscape(imageFileName)} ${imageOffset || ''}` + ); + } + + public sendGDBSet(params: string) { + return this.sendCommand(`-gdb-set ${params}`); + } + + public sendGDBShow(params: string): Promise { + return this.sendCommand(`-gdb-show ${params}`); + } + + public sendGDBExit() { + return this.sendCommand('-gdb-exit'); + } + + protected nextToken() { + return this.token++; + } +} diff --git a/src/cdtDebugAdapter/adapter/GDBDebugSession.ts b/src/cdtDebugAdapter/adapter/GDBDebugSession.ts new file mode 100644 index 000000000..fa5c71483 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/GDBDebugSession.ts @@ -0,0 +1,2195 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import * as os from "os"; +import * as path from "path"; +import * as fs from "fs"; +import { + DebugSession, + Handles, + InitializedEvent, + Logger, + logger, + LoggingDebugSession, + OutputEvent, + Response, + Scope, + Source, + StackFrame, + TerminatedEvent, +} from "@vscode/debugadapter"; +import { DebugProtocol } from "@vscode/debugprotocol"; +import { GDBBackend } from "./GDBBackend"; +import * as mi from "./mi"; +import { + sendDataReadMemoryBytes, + sendDataDisassemble, + sendDataWriteMemoryBytes, +} from "./mi/data"; +import { StoppedEvent } from "./stoppedEvent"; +import { VarObjType } from "./varManager"; +import { createEnvValues, getGdbCwd } from "./util"; + +export interface RequestArguments extends DebugProtocol.LaunchRequestArguments { + gdb?: string; + gdbArguments?: string[]; + gdbAsync?: boolean; + gdbNonStop?: boolean; + // defaults to the environment of the process of the adapter + environment?: Record; + program: string; + // defaults to dirname of the program, if present or the cwd of the process of the adapter + cwd?: string; + verbose?: boolean; + logFile?: string; + openGdbConsole?: boolean; + initCommands?: string[]; + hardwareBreakpoint?: boolean; + sessionID?: string; +} + +export interface LaunchRequestArguments extends RequestArguments { + arguments?: string; +} + +export interface AttachRequestArguments extends RequestArguments { + processId: string; +} + +export interface FrameReference { + threadId: number; + frameId: number; +} + +export interface FrameVariableReference { + type: "frame"; + frameHandle: number; +} + +export interface ObjectVariableReference { + type: "object"; + frameHandle: number; + varobjName: string; +} + +export interface RegisterVariableReference { + type: "registers"; + frameHandle: number; + regname?: string; +} + +export type VariableReference = + | FrameVariableReference + | ObjectVariableReference + | RegisterVariableReference; + +export interface MemoryRequestArguments { + address: string; + length: number; + offset?: number; +} + +/** + * Response for our custom 'cdt-gdb-adapter/Memory' request. + */ +export interface MemoryContents { + /* Hex-encoded string of bytes. */ + data: string; + address: string; +} + +export interface MemoryResponse extends Response { + body: MemoryContents; +} + +export interface CDTDisassembleArguments + extends DebugProtocol.DisassembleArguments { + /** + * Memory reference to the end location containing the instructions to disassemble. When this + * optional setting is provided, the minimum number of lines needed to get to the endMemoryReference + * is used. + */ + endMemoryReference: string; +} + +class ThreadWithStatus implements DebugProtocol.Thread { + id: number; + name: string; + running: boolean; + constructor(id: number, name: string, running: boolean) { + this.id = id; + this.name = name; + this.running = running; + } +} + +// Allow a single number for ignore count or the form '> [number]' +const ignoreCountRegex = /\s|>/g; +const arrayRegex = /.*\[[\d]+\].*/; +const arrayChildRegex = /[\d]+/; +const numberRegex = /^-?\d+(?:\.\d*)?$/; // match only numbers (integers and floats) +const cNumberTypeRegex = /\b(?:char|short|int|long|float|double)$/; // match C number types +const cBoolRegex = /\bbool$/; // match boolean + +export function hexToBase64(hex: string): string { + // The buffer will ignore incomplete bytes (unpaired digits), so we need to catch that early + if (hex.length % 2 !== 0) { + throw new Error("Received memory with incomplete bytes."); + } + const base64 = Buffer.from(hex, "hex").toString("base64"); + // If the hex input includes characters that are not hex digits, Buffer.from() will return an empty buffer, and the base64 string will be empty. + if (base64.length === 0 && hex.length !== 0) { + throw new Error("Received ill-formed hex input: " + hex); + } + return base64; +} + +export function base64ToHex(base64: string): string { + const buffer = Buffer.from(base64, "base64"); + // The caller likely passed in a value that left dangling bits that couldn't be assigned to a full byte and so + // were ignored by Buffer. We can't be sure what the client thought they wanted to do with those extra bits, so fail here. + if (buffer.length === 0 || !buffer.toString("base64").startsWith(base64)) { + throw new Error("Received ill-formed base64 input: " + base64); + } + return buffer.toString("hex"); +} + +export class GDBDebugSession extends LoggingDebugSession { + /** + * Initial (aka default) configuration for launch/attach request + * typically supplied with the --config command line argument. + */ + protected static defaultRequestArguments?: any; + + /** + * Frozen configuration for launch/attach request + * typically supplied with the --config-frozen command line argument. + */ + protected static frozenRequestArguments?: { request?: string }; + + protected gdb: GDBBackend = this.createBackend(); + protected isAttach = false; + // isRunning === true means there are no threads stopped. + protected isRunning = false; + + protected supportsRunInTerminalRequest = false; + protected supportsGdbConsole = false; + + protected isPostMortem = false; + + /* A reference to the logger to be used by subclasses */ + protected logger: Logger.Logger; + + protected frameHandles = new Handles(); + protected variableHandles = new Handles(); + protected functionBreakpoints: string[] = []; + protected logPointMessages: { [key: string]: string } = {}; + + protected threads: ThreadWithStatus[] = []; + + // promise that resolves once the target stops so breakpoints can be inserted + protected waitPaused?: (value?: void | PromiseLike) => void; + // the thread id that we were waiting for + protected waitPausedThreadId = 0; + // set to true if the target was interrupted where inteneded, and should + // therefore be resumed after breakpoints are inserted. + protected waitPausedNeeded = false; + protected isInitialized = false; + + constructor() { + super(); + this.logger = logger; + } + + /** + * Main entry point + */ + public static run(debugSession: typeof GDBDebugSession) { + GDBDebugSession.processArgv(process.argv.slice(2)); + DebugSession.run(debugSession); + } + + /** + * Parse an optional config file which is a JSON string of launch/attach request arguments. + * The config can be a response file by starting with an @. + */ + public static processArgv(args: string[]) { + args.forEach(function (val, _index, _array) { + const configMatch = /^--config(-frozen)?=(.*)$/.exec(val); + if (configMatch) { + let configJson; + const configStr = configMatch[2]; + if (configStr.startsWith("@")) { + const configFile = configStr.slice(1); + configJson = JSON.parse(fs.readFileSync(configFile).toString("utf8")); + } else { + configJson = JSON.parse(configStr); + } + if (configMatch[1]) { + GDBDebugSession.frozenRequestArguments = configJson; + } else { + GDBDebugSession.defaultRequestArguments = configJson; + } + } + }); + } + + /** + * Apply the initial and frozen launch/attach request arguments. + * @param request the default request type to return if request type is not frozen + * @param args the arguments from the user to apply initial and frozen arguments to. + * @returns resolved request type and the resolved arguments + */ + protected applyRequestArguments( + request: "launch" | "attach", + args: LaunchRequestArguments | AttachRequestArguments + ): ["launch" | "attach", LaunchRequestArguments | AttachRequestArguments] { + const frozenRequest = GDBDebugSession.frozenRequestArguments?.request; + if (frozenRequest === "launch" || frozenRequest === "attach") { + request = frozenRequest; + } + + return [ + request, + { + ...GDBDebugSession.defaultRequestArguments, + ...args, + ...GDBDebugSession.frozenRequestArguments, + }, + ]; + } + + protected createBackend(): GDBBackend { + return new GDBBackend(); + } + + /** + * Handle requests not defined in the debug adapter protocol. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected customRequest( + command: string, + response: DebugProtocol.Response, + args: any + ): void { + if (command === "cdt-gdb-adapter/Memory") { + this.memoryRequest(response as MemoryResponse, args); + // This custom request exists to allow tests in this repository to run arbitrary commands + // Use at your own risk! + } else if (command === "cdt-gdb-tests/executeCommand") { + const consoleOutput: string[] = []; + const consoleOutputListener = (line: string) => consoleOutput.push(line); + // Listens the console output for test and controls purpose during the + // test command execution. Boundry of the console output not guaranteed. + this.gdb.addListener("consoleStreamOutput", consoleOutputListener); + this.gdb + .sendCommand(args.command) + .then((result) => { + response.body = { + status: "Ok", + result, + console: consoleOutput, + }; + this.sendResponse(response); + }) + .catch((e) => { + const message = + e instanceof Error + ? e.message + : `Encountered a problem executing ${args.command}`; + this.sendErrorResponse(response, 1, message); + }) + .finally(() => { + this.gdb.removeListener("consoleStreamOutput", consoleOutputListener); + }); + } else { + return super.customRequest(command, response, args); + } + } + + protected initializeRequest( + response: DebugProtocol.InitializeResponse, + args: DebugProtocol.InitializeRequestArguments + ): void { + this.supportsRunInTerminalRequest = + args.supportsRunInTerminalRequest === true; + this.supportsGdbConsole = + os.platform() === "linux" && this.supportsRunInTerminalRequest; + response.body = response.body || {}; + response.body.supportsConfigurationDoneRequest = true; + response.body.supportsSetVariable = true; + response.body.supportsConditionalBreakpoints = true; + response.body.supportsHitConditionalBreakpoints = true; + response.body.supportsLogPoints = true; + response.body.supportsFunctionBreakpoints = true; + response.body.supportsDisassembleRequest = true; + response.body.supportsReadMemoryRequest = true; + response.body.supportsWriteMemoryRequest = true; + response.body.supportsSteppingGranularity = true; + this.sendResponse(response); + } + + protected async attachOrLaunchRequest( + response: DebugProtocol.Response, + request: "launch" | "attach", + args: LaunchRequestArguments | AttachRequestArguments + ) { + logger.setup( + args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn, + args.logFile || false + ); + + this.gdb.on("consoleStreamOutput", (output, category) => { + this.sendEvent(new OutputEvent(output, category)); + }); + + this.gdb.on("execAsync", (resultClass, resultData) => + this.handleGDBAsync(resultClass, resultData) + ); + this.gdb.on("notifyAsync", (resultClass, resultData) => + this.handleGDBNotify(resultClass, resultData) + ); + + await this.spawn(args); + if (!args.program) { + this.sendErrorResponse( + response, + 1, + "The program must be specified in the request arguments" + ); + return; + } + await this.gdb.sendFileExecAndSymbols(args.program); + await this.gdb.sendEnablePrettyPrint(); + + if (request === "attach") { + this.isAttach = true; + const attachArgs = args as AttachRequestArguments; + await mi.sendTargetAttachRequest(this.gdb, { + pid: attachArgs.processId, + }); + this.sendEvent( + new OutputEvent(`attached to process ${attachArgs.processId}`) + ); + } + + await this.gdb.sendCommands(args.initCommands); + + if (request === "launch") { + const launchArgs = args as LaunchRequestArguments; + if (launchArgs.arguments) { + await mi.sendExecArguments(this.gdb, { + arguments: launchArgs.arguments, + }); + } + } + this.sendEvent(new InitializedEvent()); + this.sendResponse(response); + this.isInitialized = true; + } + + protected async attachRequest( + response: DebugProtocol.AttachResponse, + args: AttachRequestArguments + ): Promise { + try { + const [request, resolvedArgs] = this.applyRequestArguments( + "attach", + args + ); + await this.attachOrLaunchRequest(response, request, resolvedArgs); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async launchRequest( + response: DebugProtocol.LaunchResponse, + args: LaunchRequestArguments + ): Promise { + try { + const [request, resolvedArgs] = this.applyRequestArguments( + "launch", + args + ); + await this.attachOrLaunchRequest(response, request, resolvedArgs); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async spawn(args: LaunchRequestArguments | AttachRequestArguments) { + if (args.openGdbConsole) { + if (!this.supportsGdbConsole) { + logger.warn( + "cdt-gdb-adapter: openGdbConsole is not supported on this platform" + ); + } else if ( + !(await this.gdb.supportsNewUi( + args.gdb, + getGdbCwd(args), + args.environment + )) + ) { + logger.warn( + `cdt-gdb-adapter: new-ui command not detected (${args.gdb || "gdb"})` + ); + } else { + logger.warn( + "cdt-gdb-adapter: spawning gdb console in client terminal is not supported" + ); + } + } + return this.gdb.spawn(args); + } + + protected async setBreakPointsRequest( + response: DebugProtocol.SetBreakpointsResponse, + args: DebugProtocol.SetBreakpointsArguments + ): Promise { + this.waitPausedNeeded = this.isRunning; + if (this.waitPausedNeeded) { + // Need to pause first + const waitPromise = new Promise((resolve) => { + this.waitPaused = resolve; + }); + if (this.gdb.isNonStopMode()) { + const threadInfo = await mi.sendThreadInfoRequest(this.gdb, {}); + + this.waitPausedThreadId = parseInt(threadInfo["current-thread-id"], 10); + this.gdb.pause(this.waitPausedThreadId); + } else { + this.gdb.pause(); + } + await waitPromise; + } + + try { + // Need to get the list of current breakpoints in the file and then make sure + // that we end up with the requested set of breakpoints for that file + // deleting ones not requested and inserting new ones. + + const result = await mi.sendBreakList(this.gdb); + const file = args.source.path as string; + const gdbOriginalLocationPrefix = await mi.sourceBreakpointLocation( + this.gdb, + file + ); + const gdbbps = result.BreakpointTable.body.filter((gdbbp) => { + // Ignore "children" breakpoint of entries + if (gdbbp.number.includes(".")) { + return false; + } + + // Ignore other files + if (!gdbbp["original-location"]) { + return false; + } + if (!gdbbp["original-location"].startsWith(gdbOriginalLocationPrefix)) { + return false; + } + + // Ignore function breakpoints + return this.functionBreakpoints.indexOf(gdbbp.number) === -1; + }); + + const { resolved, deletes } = this.resolveBreakpoints( + args.breakpoints || [], + gdbbps, + (vsbp, gdbbp) => { + // Always invalidate hit conditions as they have a one-way mapping to gdb ignore and temporary + if (vsbp.hitCondition) { + return false; + } + + // Ensure we can compare undefined and empty strings + const vsbpCond = vsbp.condition || undefined; + const gdbbpCond = gdbbp.cond || undefined; + + // Check with original-location so that relocated breakpoints are properly matched + const gdbOriginalLocation = `${gdbOriginalLocationPrefix}${vsbp.line}`; + return !!( + gdbbp["original-location"] === gdbOriginalLocation && + vsbpCond === gdbbpCond + ); + } + ); + + // Delete before insert to avoid breakpoint clashes in gdb + if (deletes.length > 0) { + await mi.sendBreakDelete(this.gdb, { breakpoints: deletes }); + deletes.forEach( + (breakpoint) => delete this.logPointMessages[breakpoint] + ); + } + + // Reset logPoints + this.logPointMessages = {}; + + // Set up logpoint messages and return a formatted breakpoint for the response body + const createState = ( + vsbp: DebugProtocol.SourceBreakpoint, + gdbbp: mi.MIBreakpointInfo + ): DebugProtocol.Breakpoint => { + if (vsbp.logMessage) { + this.logPointMessages[gdbbp.number] = vsbp.logMessage; + } + + let line = 0; + if (gdbbp.line) { + line = parseInt(gdbbp.line, 10); + } else if (vsbp.line) { + line = vsbp.line; + } + + return { + id: parseInt(gdbbp.number, 10), + line, + verified: true, + }; + }; + + const actual: DebugProtocol.Breakpoint[] = []; + + for (const bp of resolved) { + if (bp.gdbbp) { + actual.push(createState(bp.vsbp, bp.gdbbp)); + continue; + } + + let temporary = false; + let ignoreCount: number | undefined; + const vsbp = bp.vsbp; + if (vsbp.hitCondition !== undefined) { + ignoreCount = parseInt( + vsbp.hitCondition.replace(ignoreCountRegex, ""), + 10 + ); + if (isNaN(ignoreCount)) { + this.sendEvent( + new OutputEvent( + `Unable to decode expression: ${vsbp.hitCondition}` + ) + ); + continue; + } + + // Allow hit condition continuously above the count + temporary = !vsbp.hitCondition.startsWith(">"); + if (temporary) { + // The expression is not 'greater than', decrease ignoreCount to match + ignoreCount--; + } + } + + try { + const line = vsbp.line.toString(); + const options = await this.gdb.getBreakpointOptions( + { + locationType: "source", + source: file, + line, + }, + { + condition: vsbp.condition, + temporary, + ignoreCount, + hardware: this.gdb.isUseHWBreakpoint(), + } + ); + const gdbbp = await mi.sendSourceBreakpointInsert( + this.gdb, + file, + line, + options + ); + actual.push(createState(vsbp, gdbbp.bkpt)); + } catch (err) { + actual.push({ + verified: false, + message: err instanceof Error ? err.message : String(err), + } as DebugProtocol.Breakpoint); + } + } + + response.body = { + breakpoints: actual, + }; + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + + if (this.waitPausedNeeded) { + if (this.gdb.isNonStopMode()) { + mi.sendExecContinue(this.gdb, this.waitPausedThreadId); + } else { + mi.sendExecContinue(this.gdb); + } + } + } + + protected async setFunctionBreakPointsRequest( + response: DebugProtocol.SetFunctionBreakpointsResponse, + args: DebugProtocol.SetFunctionBreakpointsArguments + ) { + this.waitPausedNeeded = this.isRunning; + if (this.waitPausedNeeded) { + // Need to pause first + const waitPromise = new Promise((resolve) => { + this.waitPaused = resolve; + }); + if (this.gdb.isNonStopMode()) { + const threadInfo = await mi.sendThreadInfoRequest(this.gdb, {}); + + this.waitPausedThreadId = parseInt(threadInfo["current-thread-id"], 10); + this.gdb.pause(this.waitPausedThreadId); + } else { + this.gdb.pause(); + } + await waitPromise; + } + + try { + const result = await mi.sendBreakList(this.gdb); + const gdbbps = result.BreakpointTable.body.filter((gdbbp) => { + // Only function breakpoints + return this.functionBreakpoints.indexOf(gdbbp.number) > -1; + }); + + const { resolved, deletes } = this.resolveBreakpoints( + args.breakpoints, + gdbbps, + (vsbp, gdbbp) => { + // Always invalidate hit conditions as they have a one-way mapping to gdb ignore and temporary + if (vsbp.hitCondition) { + return false; + } + + // Ensure we can compare undefined and empty strings + const vsbpCond = vsbp.condition || undefined; + const gdbbpCond = gdbbp.cond || undefined; + + const originalLocation = mi.functionBreakpointLocation( + this.gdb, + vsbp.name + ); + return !!( + gdbbp["original-location"] === originalLocation && + vsbpCond === gdbbpCond + ); + } + ); + + // Delete before insert to avoid breakpoint clashes in gdb + if (deletes.length > 0) { + await mi.sendBreakDelete(this.gdb, { breakpoints: deletes }); + this.functionBreakpoints = this.functionBreakpoints.filter( + (fnbp) => deletes.indexOf(fnbp) === -1 + ); + } + + const createActual = ( + breakpoint: mi.MIBreakpointInfo + ): DebugProtocol.Breakpoint => ({ + id: parseInt(breakpoint.number, 10), + verified: true, + }); + + const actual: DebugProtocol.Breakpoint[] = []; + + for (const bp of resolved) { + if (bp.gdbbp) { + actual.push(createActual(bp.gdbbp)); + continue; + } + + try { + const options = await this.gdb.getBreakpointOptions( + { + locationType: "function", + fn: bp.vsbp.name, + }, + { + hardware: this.gdb.isUseHWBreakpoint(), + } + ); + const gdbbp = await mi.sendFunctionBreakpointInsert( + this.gdb, + bp.vsbp.name, + options + ); + this.functionBreakpoints.push(gdbbp.bkpt.number); + actual.push(createActual(gdbbp.bkpt)); + } catch (err) { + actual.push({ + verified: false, + message: err instanceof Error ? err.message : String(err), + } as DebugProtocol.Breakpoint); + } + } + + response.body = { + breakpoints: actual, + }; + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + + if (this.waitPausedNeeded) { + if (this.gdb.isNonStopMode()) { + mi.sendExecContinue(this.gdb, this.waitPausedThreadId); + } else { + mi.sendExecContinue(this.gdb); + } + } + } + + /** + * Resolved which VS breakpoints needs to be installed, which + * GDB breakpoints need to be deleted and which VS breakpoints + * are already installed with which matching GDB breakpoint. + * @param vsbps VS DAP breakpoints + * @param gdbbps GDB breakpoints + * @param matchFn matcher to compare VS and GDB breakpoints + * @returns resolved -> array maintaining order of vsbps that identifies whether + * VS breakpoint has a cooresponding GDB breakpoint (gdbbp field set) or needs to be + * inserted (gdbbp field empty) + * deletes -> GDB bps ids that should be deleted because they don't match vsbps + */ + protected resolveBreakpoints( + vsbps: T[], + gdbbps: mi.MIBreakpointInfo[], + matchFn: (vsbp: T, gdbbp: mi.MIBreakpointInfo) => boolean + ): { + resolved: Array<{ vsbp: T; gdbbp?: mi.MIBreakpointInfo }>; + deletes: string[]; + } { + const resolved: Array<{ vsbp: T; gdbbp?: mi.MIBreakpointInfo }> = vsbps.map( + (vsbp) => { + return { + vsbp, + gdbbp: gdbbps.find((gdbbp) => matchFn(vsbp, gdbbp)), + }; + } + ); + + const deletes = gdbbps + .filter((gdbbp) => { + return !vsbps.find((vsbp) => matchFn(vsbp, gdbbp)); + }) + .map((gdbbp) => gdbbp.number); + + return { resolved, deletes }; + } + + protected async configurationDoneRequest( + response: DebugProtocol.ConfigurationDoneResponse, + _args: DebugProtocol.ConfigurationDoneArguments + ): Promise { + try { + this.sendEvent( + new OutputEvent( + "\n" + + "In the Debug Console view you can interact directly with GDB.\n" + + "To display the value of an expression, type that expression which can reference\n" + + "variables that are in scope. For example type '2 + 3' or the name of a variable.\n" + + "Arbitrary commands can be sent to GDB by prefixing the input with a '>',\n" + + "for example type '>show version' or '>help'.\n" + + "\n", + "console" + ) + ); + if (!this.isPostMortem) { + if (this.isAttach) { + await mi.sendExecContinue(this.gdb); + } else { + await mi.sendExecRun(this.gdb); + } + } else { + this.sendEvent(new StoppedEvent("exception", 1, true)); + } + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 100, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected convertThread(thread: mi.MIThreadInfo) { + let name = thread.name || thread.id; + + if (thread.details) { + name += ` (${thread.details})`; + } + + const running = thread.state === "running"; + + return new ThreadWithStatus(parseInt(thread.id, 10), name, running); + } + + protected async threadsRequest( + response: DebugProtocol.ThreadsResponse + ): Promise { + try { + if (!this.isRunning) { + const result = await mi.sendThreadInfoRequest(this.gdb, {}); + this.threads = result.threads + .map((thread) => this.convertThread(thread)) + .sort((a, b) => a.id - b.id); + } + + response.body = { + threads: this.threads, + }; + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async stackTraceRequest( + response: DebugProtocol.StackTraceResponse, + args: DebugProtocol.StackTraceArguments + ): Promise { + try { + const threadId = args.threadId; + const depthResult = await mi.sendStackInfoDepth(this.gdb, { + maxDepth: 100, + threadId, + }); + const depth = parseInt(depthResult.depth, 10); + const levels = args.levels + ? args.levels > depth + ? depth + : args.levels + : depth; + const lowFrame = args.startFrame || 0; + const highFrame = lowFrame + levels - 1; + const listResult = await mi.sendStackListFramesRequest(this.gdb, { + lowFrame, + highFrame, + threadId, + }); + + const stack = listResult.stack.map((frame) => { + let source; + if (frame.fullname) { + source = new Source( + path.basename(frame.file || frame.fullname), + frame.fullname + ); + } + let line; + if (frame.line) { + line = parseInt(frame.line, 10); + } + const frameHandle = this.frameHandles.create({ + threadId: args.threadId, + frameId: parseInt(frame.level, 10), + }); + const name = frame.func || frame.fullname || ""; + const sf = new StackFrame( + frameHandle, + name, + source, + line + ) as DebugProtocol.StackFrame; + sf.instructionPointerReference = frame.addr; + return sf; + }); + + response.body = { + stackFrames: stack, + totalFrames: depth, + }; + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async nextRequest( + response: DebugProtocol.NextResponse, + args: DebugProtocol.NextArguments + ): Promise { + try { + await (args.granularity === "instruction" + ? mi.sendExecNextInstruction(this.gdb, args.threadId) + : mi.sendExecNext(this.gdb, args.threadId)); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async stepInRequest( + response: DebugProtocol.StepInResponse, + args: DebugProtocol.StepInArguments + ): Promise { + try { + await (args.granularity === "instruction" + ? mi.sendExecStepInstruction(this.gdb, args.threadId) + : mi.sendExecStep(this.gdb, args.threadId)); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async stepOutRequest( + response: DebugProtocol.StepOutResponse, + args: DebugProtocol.StepOutArguments + ): Promise { + try { + await mi.sendExecFinish(this.gdb, args.threadId); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async continueRequest( + response: DebugProtocol.ContinueResponse, + args: DebugProtocol.ContinueArguments + ): Promise { + try { + await mi.sendExecContinue(this.gdb, args.threadId); + let isAllThreadsContinued; + if (this.gdb.isNonStopMode()) { + isAllThreadsContinued = args.threadId ? false : true; + } else { + isAllThreadsContinued = true; + } + response.body = { + allThreadsContinued: isAllThreadsContinued, + }; + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async pauseRequest( + response: DebugProtocol.PauseResponse, + args: DebugProtocol.PauseArguments + ): Promise { + try { + this.gdb.pause(args.threadId); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected scopesRequest( + response: DebugProtocol.ScopesResponse, + args: DebugProtocol.ScopesArguments + ): void { + const frame: FrameVariableReference = { + type: "frame", + frameHandle: args.frameId, + }; + + const registers: RegisterVariableReference = { + type: "registers", + frameHandle: args.frameId, + }; + + response.body = { + scopes: [ + new Scope("Local", this.variableHandles.create(frame), false), + new Scope("Registers", this.variableHandles.create(registers), true), + ], + }; + + this.sendResponse(response); + } + + protected async variablesRequest( + response: DebugProtocol.VariablesResponse, + args: DebugProtocol.VariablesArguments + ): Promise { + const variables = new Array(); + response.body = { + variables, + }; + try { + const ref = this.variableHandles.get(args.variablesReference); + if (!ref) { + this.sendResponse(response); + return; + } + if (ref.type === "registers") { + response.body.variables = await this.handleVariableRequestRegister(ref); + } else if (ref.type === "frame") { + response.body.variables = await this.handleVariableRequestFrame(ref); + } else if (ref.type === "object") { + response.body.variables = await this.handleVariableRequestObject(ref); + } + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async setVariableRequest( + response: DebugProtocol.SetVariableResponse, + args: DebugProtocol.SetVariableArguments + ): Promise { + try { + const ref = this.variableHandles.get(args.variablesReference); + if (!ref) { + this.sendResponse(response); + return; + } + const frame = this.frameHandles.get(ref.frameHandle); + if (!frame) { + this.sendResponse(response); + return; + } + const parentVarname = ref.type === "object" ? ref.varobjName : ""; + const varname = + parentVarname + + (parentVarname === "" ? "" : ".") + + args.name.replace(/^\[(\d+)\]/, "$1"); + const stackDepth = await mi.sendStackInfoDepth(this.gdb, { + maxDepth: 100, + }); + const depth = parseInt(stackDepth.depth, 10); + let varobj = this.gdb.varManager.getVar( + frame.frameId, + frame.threadId, + depth, + varname, + ref.type + ); + if (!varobj && ref.type === "registers") { + const varCreateResponse = await mi.sendVarCreate(this.gdb, { + expression: "$" + args.name, + frameId: frame.frameId, + threadId: frame.threadId, + }); + varobj = this.gdb.varManager.addVar( + frame.frameId, + frame.threadId, + depth, + args.name, + false, + false, + varCreateResponse, + ref.type + ); + await mi.sendVarSetFormatToHex(this.gdb, varobj.varname); + } + let assign; + if (varobj) { + assign = await mi.sendVarAssign(this.gdb, { + varname: varobj.varname, + expression: args.value, + }); + } else { + try { + assign = await mi.sendVarAssign(this.gdb, { + varname, + expression: args.value, + }); + } catch (err) { + if (parentVarname === "") { + throw err; // no recovery possible + } + const children = await mi.sendVarListChildren(this.gdb, { + name: parentVarname, + printValues: mi.MIVarPrintValues.all, + }); + for (const child of children.children) { + if (this.isChildOfClass(child)) { + const grandchildVarname = + child.name + "." + args.name.replace(/^\[(\d+)\]/, "$1"); + varobj = this.gdb.varManager.getVar( + frame.frameId, + frame.threadId, + depth, + grandchildVarname + ); + try { + assign = await mi.sendVarAssign(this.gdb, { + varname: grandchildVarname, + expression: args.value, + }); + break; + } catch (err) { + continue; // try another child + } + } + } + if (!assign) { + throw err; // no recovery possible + } + } + } + response.body = { + value: assign.value, + type: varobj ? varobj.type : undefined, + variablesReference: + varobj && parseInt(varobj.numchild, 10) > 0 + ? this.variableHandles.create({ + type: "object", + frameHandle: ref.frameHandle, + varobjName: varobj.varname, + }) + : 0, + }; + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + this.sendResponse(response); + } + + protected async evaluateRequest( + response: DebugProtocol.EvaluateResponse, + args: DebugProtocol.EvaluateArguments + ): Promise { + response.body = { + result: "Error: could not evaluate expression", + variablesReference: 0, + }; // default response + try { + if (args.frameId === undefined) { + throw new Error( + "Evaluation of expression without frameId is not supported." + ); + } + + const frame = this.frameHandles.get(args.frameId); + if (!frame) { + this.sendResponse(response); + return; + } + + if (args.expression.startsWith(">") && args.context === "repl") { + if (args.expression[1] === "-") { + await this.gdb.sendCommand(args.expression.slice(1)); + } else { + await mi.sendInterpreterExecConsole(this.gdb, { + threadId: frame.threadId, + frameId: frame.frameId, + command: args.expression.slice(1), + }); + } + response.body = { + result: "\r", + variablesReference: 0, + }; + this.sendResponse(response); + return; + } + + const stackDepth = await mi.sendStackInfoDepth(this.gdb, { + maxDepth: 100, + }); + const depth = parseInt(stackDepth.depth, 10); + let varobj = this.gdb.varManager.getVar( + frame.frameId, + frame.threadId, + depth, + args.expression + ); + if (!varobj) { + const varCreateResponse = await mi.sendVarCreate(this.gdb, { + expression: args.expression, + frameId: frame.frameId, + threadId: frame.threadId, + }); + varobj = this.gdb.varManager.addVar( + frame.frameId, + frame.threadId, + depth, + args.expression, + false, + false, + varCreateResponse + ); + } else { + const vup = await mi.sendVarUpdate(this.gdb, { + name: varobj.varname, + }); + const update = vup.changelist[0]; + if (update) { + if (update.in_scope === "true") { + if (update.name === varobj.varname) { + varobj.value = update.value; + } + } else { + this.gdb.varManager.removeVar( + frame.frameId, + frame.threadId, + depth, + varobj.varname + ); + await mi.sendVarDelete(this.gdb, { + varname: varobj.varname, + }); + const varCreateResponse = await mi.sendVarCreate(this.gdb, { + expression: args.expression, + frameId: frame.frameId, + threadId: frame.threadId, + }); + varobj = this.gdb.varManager.addVar( + frame.frameId, + frame.threadId, + depth, + args.expression, + false, + false, + varCreateResponse + ); + } + } + } + if (varobj) { + const result = + args.context === "variables" && Number(varobj.numchild) + ? await this.getChildElements(varobj, args.frameId) + : varobj.value; + response.body = { + result, + type: varobj.type, + variablesReference: + parseInt(varobj.numchild, 10) > 0 + ? this.variableHandles.create({ + type: "object", + frameHandle: args.frameId, + varobjName: varobj.varname, + }) + : 0, + }; + } + + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async getChildElements(varobj: VarObjType, frameHandle: number) { + if (Number(varobj.numchild) > 0) { + const objRef: ObjectVariableReference = { + type: "object", + frameHandle: frameHandle, + varobjName: varobj.varname, + }; + const childVariables: DebugProtocol.Variable[] = await this.handleVariableRequestObject( + objRef + ); + const value = arrayChildRegex.test(varobj.type) + ? childVariables.map((child) => + this.convertValue(child) + ) + : childVariables.reduce>( + (accum, child) => ( + (accum[child.name] = this.convertValue(child)), accum + ), + {} + ); + return JSON.stringify(value, null, 2); + } + return varobj.value; + } + + protected convertValue(variable: DebugProtocol.Variable) { + const varValue = variable.value; + const varType = String(variable.type); + if (cNumberTypeRegex.test(varType)) { + if (numberRegex.test(varValue)) { + return Number(varValue); + } else { + // probably a string/other representation + return String(varValue); + } + } else if (cBoolRegex.test(varType)) { + return Boolean(varValue); + } else { + return varValue; + } + } + + /** + * Implement the cdt-gdb-adapter/Memory request. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected async memoryRequest(response: MemoryResponse, args: any) { + try { + if (typeof args.address !== "string") { + throw new Error( + `Invalid type for 'address', expected string, got ${typeof args.address}` + ); + } + + if (typeof args.length !== "number") { + throw new Error( + `Invalid type for 'length', expected number, got ${typeof args.length}` + ); + } + + if ( + typeof args.offset !== "number" && + typeof args.offset !== "undefined" + ) { + throw new Error( + `Invalid type for 'offset', expected number or undefined, got ${typeof args.offset}` + ); + } + + const typedArgs = args as MemoryRequestArguments; + + const result = await sendDataReadMemoryBytes( + this.gdb, + typedArgs.address, + typedArgs.length, + typedArgs.offset + ); + response.body = { + data: result.memory[0].contents, + address: result.memory[0].begin, + }; + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async disassembleRequest( + response: DebugProtocol.DisassembleResponse, + args: CDTDisassembleArguments + ) { + try { + const meanSizeOfInstruction = 4; + let startOffset = 0; + let lastStartOffset = -1; + const instructions: DebugProtocol.DisassembledInstruction[] = []; + let oneIterationOnly = false; + outer_loop: while ( + instructions.length < args.instructionCount && + !oneIterationOnly + ) { + if (startOffset === lastStartOffset) { + // We have stopped getting new instructions, give up + break outer_loop; + } + lastStartOffset = startOffset; + + const fetchSize = + (args.instructionCount - instructions.length) * meanSizeOfInstruction; + + // args.memoryReference is an arbitrary expression, so let GDB do the + // math on resolving value rather than doing the addition in the adapter + try { + const stepStartAddress = `(${args.memoryReference})+${startOffset}`; + let stepEndAddress = `(${args.memoryReference})+${startOffset}+${fetchSize}`; + if (args.endMemoryReference && instructions.length === 0) { + // On the first call, if we have an end memory address use it instead of + // the approx size + stepEndAddress = args.endMemoryReference; + oneIterationOnly = true; + } + const result = await sendDataDisassemble( + this.gdb, + stepStartAddress, + stepEndAddress + ); + for (const asmInsn of result.asm_insns) { + const line: number | undefined = asmInsn.line + ? parseInt(asmInsn.line, 10) + : undefined; + const source = { + name: asmInsn.file, + path: asmInsn.fullname, + } as DebugProtocol.Source; + for (const asmLine of asmInsn.line_asm_insn) { + let funcAndOffset: string | undefined; + if (asmLine["func-name"] && asmLine.offset) { + funcAndOffset = `${asmLine["func-name"]}+${asmLine.offset}`; + } else if (asmLine["func-name"]) { + funcAndOffset = asmLine["func-name"]; + } else { + funcAndOffset = undefined; + } + const disInsn = { + address: asmLine.address, + instructionBytes: asmLine.opcodes, + instruction: asmLine.inst, + symbol: funcAndOffset, + location: source, + line, + } as DebugProtocol.DisassembledInstruction; + instructions.push(disInsn); + if (instructions.length === args.instructionCount) { + break outer_loop; + } + + const bytes = asmLine.opcodes.replace(/\s/g, ""); + startOffset += bytes.length; + } + } + } catch (err) { + // Failed to read instruction -- what best to do here? + // in other words, whose responsibility (adapter or client) + // to reissue reads in smaller chunks to find good memory + while (instructions.length < args.instructionCount) { + const badDisInsn = { + // TODO this should start at byte after last retrieved address + address: `0x${startOffset.toString(16)}`, + instruction: err instanceof Error ? err.message : String(err), + } as DebugProtocol.DisassembledInstruction; + instructions.push(badDisInsn); + startOffset += 2; + } + break outer_loop; + } + } + + if (!args.endMemoryReference) { + while (instructions.length < args.instructionCount) { + const badDisInsn = { + // TODO this should start at byte after last retrieved address + address: `0x${startOffset.toString(16)}`, + instruction: "failed to retrieve instruction", + } as DebugProtocol.DisassembledInstruction; + instructions.push(badDisInsn); + startOffset += 2; + } + } + + response.body = { instructions }; + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async readMemoryRequest( + response: DebugProtocol.ReadMemoryResponse, + args: DebugProtocol.ReadMemoryArguments + ): Promise { + try { + if (args.count) { + const result = await sendDataReadMemoryBytes( + this.gdb, + args.memoryReference, + args.count, + args.offset + ); + response.body = { + data: hexToBase64(result.memory[0].contents), + address: result.memory[0].begin, + }; + this.sendResponse(response); + } else { + this.sendResponse(response); + } + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + /** + * Implement the memoryWrite request. + */ + protected async writeMemoryRequest( + response: DebugProtocol.WriteMemoryResponse, + args: DebugProtocol.WriteMemoryArguments + ) { + try { + const { memoryReference, data } = args; + const typeofAddress = typeof memoryReference; + const typeofContent = typeof data; + if (typeofAddress !== "string") { + throw new Error( + `Invalid type for 'address', expected string, got ${typeofAddress}` + ); + } + if (typeofContent !== "string") { + throw new Error( + `Invalid type for 'content', expected string, got ${typeofContent}` + ); + } + const hexContent = base64ToHex(data); + await sendDataWriteMemoryBytes(this.gdb, memoryReference, hexContent); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async disconnectRequest( + response: DebugProtocol.DisconnectResponse, + _args: DebugProtocol.DisconnectArguments + ): Promise { + try { + await this.gdb.sendGDBExit(); + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected sendStoppedEvent( + reason: string, + threadId: number, + allThreadsStopped?: boolean + ) { + // Reset frame handles and variables for new context + this.frameHandles.reset(); + this.variableHandles.reset(); + // Send the event + this.sendEvent(new StoppedEvent(reason, threadId, allThreadsStopped)); + } + + protected handleGDBStopped(result: any) { + const getThreadId = (resultData: any) => + parseInt(resultData["thread-id"], 10); + const getAllThreadsStopped = (resultData: any) => { + return ( + !!resultData["stopped-threads"] && + resultData["stopped-threads"] === "all" + ); + }; + + switch (result.reason) { + case "exited": + case "exited-normally": + this.sendEvent(new TerminatedEvent()); + break; + case "breakpoint-hit": + if (this.logPointMessages[result.bkptno]) { + this.sendEvent(new OutputEvent(this.logPointMessages[result.bkptno])); + mi.sendExecContinue(this.gdb); + } else { + const reason = + this.functionBreakpoints.indexOf(result.bkptno) > -1 + ? "function breakpoint" + : "breakpoint"; + this.sendStoppedEvent( + reason, + getThreadId(result), + getAllThreadsStopped(result) + ); + } + break; + case "end-stepping-range": + case "function-finished": + this.sendStoppedEvent( + "step", + getThreadId(result), + getAllThreadsStopped(result) + ); + break; + case "signal-received": { + const name = result["signal-name"] || "signal"; + this.sendStoppedEvent( + name, + getThreadId(result), + getAllThreadsStopped(result) + ); + break; + } + default: + this.sendStoppedEvent( + "generic", + getThreadId(result), + getAllThreadsStopped(result) + ); + } + } + + protected handleGDBAsync(resultClass: string, resultData: any) { + const updateIsRunning = () => { + this.isRunning = this.threads.length ? true : false; + for (const thread of this.threads) { + if (!thread.running) { + this.isRunning = false; + } + } + }; + switch (resultClass) { + case "running": + if (this.gdb.isNonStopMode()) { + const id = parseInt(resultData["thread-id"], 10); + for (const thread of this.threads) { + if (thread.id === id) { + thread.running = true; + } + } + } else { + for (const thread of this.threads) { + thread.running = true; + } + } + updateIsRunning(); + break; + case "stopped": { + let suppressHandleGDBStopped = false; + if (this.gdb.isNonStopMode()) { + const id = parseInt(resultData["thread-id"], 10); + for (const thread of this.threads) { + if (thread.id === id) { + thread.running = false; + } + } + if ( + this.waitPaused && + resultData.reason === "signal-received" && + this.waitPausedThreadId === id + ) { + suppressHandleGDBStopped = true; + } + } else { + for (const thread of this.threads) { + thread.running = false; + } + if (this.waitPaused && resultData.reason === "signal-received") { + suppressHandleGDBStopped = true; + } + } + + if (this.waitPaused) { + if (!suppressHandleGDBStopped) { + // if we aren't suppressing the stopped event going + // to the client, then we also musn't resume the + // target after inserting the breakpoints + this.waitPausedNeeded = false; + } + this.waitPaused(); + this.waitPaused = undefined; + } + + const wasRunning = this.isRunning; + updateIsRunning(); + if ( + !suppressHandleGDBStopped && + (this.gdb.isNonStopMode() || (wasRunning && !this.isRunning)) + ) { + if (this.isInitialized) { + this.handleGDBStopped(resultData); + } + } + break; + } + default: + logger.warn( + `GDB unhandled async: ${resultClass}: ${JSON.stringify(resultData)}` + ); + } + } + + protected handleGDBNotify(notifyClass: string, notifyData: any) { + switch (notifyClass) { + case "thread-created": + this.threads.push(this.convertThread(notifyData)); + break; + case "thread-exited": { + const thread: mi.MIThreadInfo = notifyData; + const exitId = parseInt(thread.id, 10); + this.threads = this.threads.filter((t) => t.id !== exitId); + break; + } + case "thread-selected": + case "thread-group-added": + case "thread-group-started": + case "thread-group-exited": + case "library-loaded": + case "breakpoint-modified": + case "breakpoint-deleted": + case "cmd-param-changed": + // Known unhandled notifies + break; + default: + logger.warn( + `GDB unhandled notify: ${notifyClass}: ${JSON.stringify(notifyData)}` + ); + } + } + + protected async handleVariableRequestFrame( + ref: FrameVariableReference + ): Promise { + // initialize variables array and dereference the frame handle + const variables: DebugProtocol.Variable[] = []; + const frame = this.frameHandles.get(ref.frameHandle); + if (!frame) { + return Promise.resolve(variables); + } + + // vars used to determine if we should call sendStackListVariables() + let callStack = false; + let numVars = 0; + + // stack depth necessary for differentiating between similarly named variables at different stack depths + const stackDepth = await mi.sendStackInfoDepth(this.gdb, { + maxDepth: 100, + }); + const depth = parseInt(stackDepth.depth, 10); + + // array of varnames to delete. Cannot delete while iterating through the vars array below. + const toDelete = new Array(); + + // get the list of vars we need to update for this frameId/threadId/depth tuple + const vars = this.gdb.varManager.getVars( + frame.frameId, + frame.threadId, + depth + ); + if (vars) { + for (const varobj of vars) { + // ignore expressions and child entries + if (varobj.isVar && !varobj.isChild) { + // request update from GDB + const vup = await mi.sendVarUpdate(this.gdb, { + name: varobj.varname, + }); + // if changelist is length 0, update is undefined + const update = vup.changelist[0]; + let pushVar = true; + if (update) { + if (update.in_scope === "true") { + numVars++; + if (update.name === varobj.varname) { + // don't update the parent value to a child's value + varobj.value = update.value; + } + } else { + // var is out of scope, delete it and call sendStackListVariables() later + callStack = true; + pushVar = false; + toDelete.push(update.name); + } + } else if (varobj.value) { + // value hasn't updated but it's still in scope + numVars++; + } + // only push entries to the result that aren't being deleted + if (pushVar) { + let value = varobj.value; + // if we have an array parent entry, we need to display the address. + if (arrayRegex.test(varobj.type)) { + value = await this.getAddr(varobj); + } + variables.push({ + name: varobj.expression, + evaluateName: varobj.expression, + value, + type: varobj.type, + memoryReference: `&(${varobj.expression})`, + variablesReference: + parseInt(varobj.numchild, 10) > 0 + ? this.variableHandles.create({ + type: "object", + frameHandle: ref.frameHandle, + varobjName: varobj.varname, + }) + : 0, + }); + } + } + } + // clean up out of scope entries + for (const varname of toDelete) { + await this.gdb.varManager.removeVar( + frame.frameId, + frame.threadId, + depth, + varname + ); + } + } + // if we had out of scope entries or no entries in the frameId/threadId/depth tuple, query GDB for new ones + if (callStack === true || numVars === 0) { + const result = await mi.sendStackListVariables(this.gdb, { + thread: frame.threadId, + frame: frame.frameId, + printValues: "simple-values", + }); + for (const variable of result.variables) { + let varobj = this.gdb.varManager.getVar( + frame.frameId, + frame.threadId, + depth, + variable.name + ); + if (!varobj) { + // create var in GDB and store it in the varMgr + const varCreateResponse = await mi.sendVarCreate(this.gdb, { + expression: variable.name, + frameId: frame.frameId, + threadId: frame.threadId, + }); + varobj = this.gdb.varManager.addVar( + frame.frameId, + frame.threadId, + depth, + variable.name, + true, + false, + varCreateResponse + ); + } else { + // var existed as an expression before. Now it's a variable too. + varobj = await this.gdb.varManager.updateVar( + frame.frameId, + frame.threadId, + depth, + varobj + ); + varobj.isVar = true; + } + let value = varobj.value; + // if we have an array parent entry, we need to display the address. + if (arrayRegex.test(varobj.type)) { + value = await this.getAddr(varobj); + } + variables.push({ + name: varobj.expression, + evaluateName: varobj.expression, + value, + type: varobj.type, + memoryReference: `&(${varobj.expression})`, + variablesReference: + parseInt(varobj.numchild, 10) > 0 + ? this.variableHandles.create({ + type: "object", + frameHandle: ref.frameHandle, + varobjName: varobj.varname, + }) + : 0, + }); + } + } + return Promise.resolve(variables); + } + + protected async handleVariableRequestObject( + ref: ObjectVariableReference + ): Promise { + // initialize variables array and dereference the frame handle + const variables: DebugProtocol.Variable[] = []; + const frame = this.frameHandles.get(ref.frameHandle); + if (!frame) { + return Promise.resolve(variables); + } + + // fetch stack depth to obtain frameId/threadId/depth tuple + const stackDepth = await mi.sendStackInfoDepth(this.gdb, { + maxDepth: 100, + }); + const depth = parseInt(stackDepth.depth, 10); + // we need to keep track of children and the parent varname in GDB + let children; + let parentVarname = ref.varobjName; + + // if a varobj exists, use the varname stored there + const varobj = this.gdb.varManager.getVarByName( + frame.frameId, + frame.threadId, + depth, + ref.varobjName + ); + if (varobj) { + children = await mi.sendVarListChildren(this.gdb, { + name: varobj.varname, + printValues: mi.MIVarPrintValues.all, + }); + parentVarname = varobj.varname; + } else { + // otherwise use the parent name passed in the variable reference + children = await mi.sendVarListChildren(this.gdb, { + name: ref.varobjName, + printValues: mi.MIVarPrintValues.all, + }); + } + // Grab the full path of parent. + const topLevelPathExpression = + varobj?.expression ?? (await this.getFullPathExpression(parentVarname)); + + // iterate through the children + for (const child of children.children) { + // check if we're dealing with a C++ object. If we are, we need to fetch the grandchildren instead. + const isClass = this.isChildOfClass(child); + if (isClass) { + const name = `${parentVarname}.${child.exp}`; + const objChildren = await mi.sendVarListChildren(this.gdb, { + name, + printValues: mi.MIVarPrintValues.all, + }); + // Append the child path to the top level full path. + const parentClassName = `${topLevelPathExpression}.${child.exp}`; + for (const objChild of objChildren.children) { + const childName = `${name}.${objChild.exp}`; + variables.push({ + name: objChild.exp, + evaluateName: `${parentClassName}.${objChild.exp}`, + value: objChild.value ? objChild.value : objChild.type, + type: objChild.type, + variablesReference: + parseInt(objChild.numchild, 10) > 0 + ? this.variableHandles.create({ + type: "object", + frameHandle: ref.frameHandle, + varobjName: childName, + }) + : 0, + }); + } + } else { + // check if we're dealing with an array + let name = `${ref.varobjName}.${child.exp}`; + let varobjName = name; + let value = child.value ? child.value : child.type; + const isArrayParent = arrayRegex.test(child.type); + const isArrayChild = + varobj !== undefined + ? arrayRegex.test(varobj.type) && arrayChildRegex.test(child.exp) + : false; + if (isArrayChild) { + // update the display name for array elements to have square brackets + name = `[${child.exp}]`; + } + if (isArrayParent || isArrayChild) { + // can't use a relative varname (eg. var1.a.b.c) to create/update a new var so fetch and track these + // vars by evaluating their path expression from GDB + const fullPath = await this.getFullPathExpression(child.name); + // create or update the var in GDB + let arrobj = this.gdb.varManager.getVar( + frame.frameId, + frame.threadId, + depth, + fullPath + ); + if (!arrobj) { + const varCreateResponse = await mi.sendVarCreate(this.gdb, { + expression: fullPath, + frameId: frame.frameId, + threadId: frame.threadId, + }); + arrobj = this.gdb.varManager.addVar( + frame.frameId, + frame.threadId, + depth, + fullPath, + true, + false, + varCreateResponse + ); + } else { + arrobj = await this.gdb.varManager.updateVar( + frame.frameId, + frame.threadId, + depth, + arrobj + ); + } + // if we have an array parent entry, we need to display the address. + if (isArrayParent) { + value = await this.getAddr(arrobj); + } + arrobj.isChild = true; + varobjName = arrobj.varname; + } + const variableName = isArrayChild ? name : child.exp; + const evaluateName = + isArrayParent || isArrayChild + ? await this.getFullPathExpression(child.name) + : `${topLevelPathExpression}.${child.exp}`; + variables.push({ + name: variableName, + evaluateName, + value, + type: child.type, + variablesReference: + parseInt(child.numchild, 10) > 0 + ? this.variableHandles.create({ + type: "object", + frameHandle: ref.frameHandle, + varobjName, + }) + : 0, + }); + } + } + return Promise.resolve(variables); + } + + /** Query GDB using varXX name to get complete variable name */ + protected async getFullPathExpression(inputVarName: string) { + const exprResponse = await mi.sendVarInfoPathExpression( + this.gdb, + inputVarName + ); + // result from GDB looks like (parentName).field so remove (). + return exprResponse.path_expr.replace(/[()]/g, ""); + } + + // Register view + // Assume that the register name are unchanging over time, and the same across all threadsf + private registerMap = new Map(); + private registerMapReverse = new Map(); + protected async handleVariableRequestRegister( + ref: RegisterVariableReference + ): Promise { + // initialize variables array and dereference the frame handle + const variables: DebugProtocol.Variable[] = []; + const frame = this.frameHandles.get(ref.frameHandle); + if (!frame) { + return Promise.resolve(variables); + } + + if (this.registerMap.size === 0) { + const result_names = await mi.sendDataListRegisterNames(this.gdb, { + frameId: frame.frameId, + threadId: frame.threadId, + }); + let idx = 0; + const registerNames = result_names["register-names"]; + for (const regs of registerNames) { + if (regs !== "") { + this.registerMap.set(regs, idx); + this.registerMapReverse.set(idx, regs); + } + idx++; + } + } + + const result_values = await mi.sendDataListRegisterValues(this.gdb, { + fmt: "x", + frameId: frame.frameId, + threadId: frame.threadId, + }); + const reg_values = result_values["register-values"]; + for (const n of reg_values) { + const id = n.number; + const reg = this.registerMapReverse.get(parseInt(id)); + if (reg) { + const val = n.value; + const res: DebugProtocol.Variable = { + name: reg, + evaluateName: "$" + reg, + value: val, + variablesReference: 0, + }; + variables.push(res); + } else { + throw new Error("Unable to parse response for reg. values"); + } + } + + return Promise.resolve(variables); + } + + protected async getAddr(varobj: VarObjType) { + const addr = await mi.sendDataEvaluateExpression( + this.gdb, + `&(${varobj.expression})` + ); + return addr.value ? addr.value : varobj.value; + } + + protected isChildOfClass(child: mi.MIVarChild): boolean { + return ( + child.type === undefined && + child.value === "" && + (child.exp === "public" || + child.exp === "protected" || + child.exp === "private") + ); + } +} diff --git a/src/cdtDebugAdapter/adapter/GDBTargetDebugSession.ts b/src/cdtDebugAdapter/adapter/GDBTargetDebugSession.ts new file mode 100644 index 000000000..545ddd104 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/GDBTargetDebugSession.ts @@ -0,0 +1,577 @@ +/********************************************************************* + * Copyright (c) 2019 Kichwa Coders and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ + +import { GDBDebugSession, RequestArguments } from "./GDBDebugSession"; +import { + InitializedEvent, + Logger, + logger, + OutputEvent, +} from "@vscode/debugadapter"; +import * as mi from "./mi"; +import * as os from "os"; +import { DebugProtocol } from "@vscode/debugprotocol"; +import { spawn, ChildProcess } from "child_process"; +import { SerialPort, ReadlineParser } from "serialport"; +import { Socket } from "net"; +import { createEnvValues, getGdbCwd } from "./util"; + +interface UARTArguments { + // Path to the serial port connected to the UART on the board. + serialPort?: string; + // Target TCP port on the host machine to attach socket to print UART output (defaults to 3456) + socketPort?: string; + // Baud Rate (in bits/s) of the serial port to be opened (defaults to 115200). + baudRate?: number; + // The number of bits in each character of data sent across the serial line (defaults to 8). + characterSize?: 5 | 6 | 7 | 8; + // The type of parity check enabled with the transmitted data (defaults to "none" - no parity bit sent) + parity?: "none" | "even" | "odd" | "mark" | "space"; + // The number of stop bits sent to allow the receiver to detect the end of characters and resynchronize with the character stream (defaults to 1). + stopBits?: 1 | 1.5 | 2; + // The handshaking method used for flow control across the serial line (defaults to "none" - no handshaking) + handshakingMethod?: "none" | "XON/XOFF" | "RTS/CTS"; + // The EOL character used to parse the UART output line-by-line. + eolCharacter?: "LF" | "CRLF"; +} + +export interface TargetAttachArguments { + // Target type default is "remote" + type?: string; + // Target parameters would be something like "localhost:12345", defaults + // to [`${host}:${port}`] + parameters?: string[]; + // Target host to connect to, defaults to 'localhost', ignored if parameters is set + host?: string; + // Target port to connect to, ignored if parameters is set + port?: string; + // Target connect commands - if specified used in preference of type, parameters, host, target + connectCommands?: string[]; + // Settings related to displaying UART output in the debug console + uart?: UARTArguments; +} + +export interface TargetLaunchArguments extends TargetAttachArguments { + // The executable for the target server to launch (e.g. gdbserver or JLinkGDBServerCLExe), + // defaults to 'gdbserver --once :0 ${args.program}' (requires gdbserver >= 7.3) + server?: string; + serverParameters?: string[]; + // Specifies the working directory of gdbserver, defaults to environment in RequestArguments + environment?: Record; + // Regular expression to extract port from by examinging stdout/err of server. + // Once server is launched, port will be set to this if port is not set. + // defaults to matching a string like 'Listening on port 41551' which is what gdbserver provides + // Ignored if port or parameters is set + serverPortRegExp?: string; + // Delay after startup before continuing launch, in milliseconds. If serverPortRegExp is + // provided, it is the delay after that regexp is seen. + serverStartupDelay?: number; + // Automatically kill the launched server when client issues a disconnect (default: true) + automaticallyKillServer?: boolean; + // Specifies the working directory of gdbserver, defaults to cwd in RequestArguments + cwd?: string; +} + +export interface ImageAndSymbolArguments { + // If specified, a symbol file to load at the given (optional) offset + symbolFileName?: string; + symbolOffset?: string; + // If specified, an image file to load at the given (optional) offset + imageFileName?: string; + imageOffset?: string; +} + +export interface TargetAttachRequestArguments extends RequestArguments { + target?: TargetAttachArguments; + imageAndSymbols?: ImageAndSymbolArguments; + // Optional commands to issue between loading image and resuming target + preRunCommands?: string[]; +} + +export interface TargetLaunchRequestArguments + extends TargetAttachRequestArguments { + target?: TargetLaunchArguments; + imageAndSymbols?: ImageAndSymbolArguments; + // Optional commands to issue between loading image and resuming target + preRunCommands?: string[]; +} + +export class GDBTargetDebugSession extends GDBDebugSession { + protected gdbserver?: ChildProcess; + protected killGdbServer = true; + + // Serial Port to capture UART output across the serial line + protected serialPort?: SerialPort; + // Socket to listen on a TCP port to capture UART output + protected socket?: Socket; + + /** + * Define the target type here such that we can run the "disconnect" + * command when servicing the disconnect request if the target type + * is remote. + */ + protected targetType?: string; + + protected async attachOrLaunchRequest( + response: DebugProtocol.Response, + request: "launch" | "attach", + args: TargetLaunchRequestArguments | TargetAttachRequestArguments + ) { + this.setupCommonLoggerAndHandlers(args); + + if ( + args.sessionID === "gdbstub.debug.session.ws" || + args.sessionID === "core-dump.debug.session.ws" + ) { + this.isPostMortem = true; + } + + if (request === "launch") { + const launchArgs = args as TargetLaunchRequestArguments; + if ( + launchArgs.target?.serverParameters === undefined && + !launchArgs.program + ) { + this.sendErrorResponse( + response, + 1, + "The program must be specified in the launch request arguments" + ); + return; + } + await this.startGDBServer(launchArgs); + } + + await this.startGDBAndAttachToTarget(response, args); + } + + protected async launchRequest( + response: DebugProtocol.LaunchResponse, + args: TargetLaunchRequestArguments + ): Promise { + try { + const [request, resolvedArgs] = this.applyRequestArguments( + "launch", + args + ); + await this.attachOrLaunchRequest(response, request, resolvedArgs); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async attachRequest( + response: DebugProtocol.AttachResponse, + args: TargetAttachRequestArguments + ): Promise { + try { + const [request, resolvedArgs] = this.applyRequestArguments( + "attach", + args + ); + await this.attachOrLaunchRequest(response, request, resolvedArgs); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected setupCommonLoggerAndHandlers(args: TargetLaunchRequestArguments) { + logger.setup( + args.verbose ? Logger.LogLevel.Verbose : Logger.LogLevel.Warn, + args.logFile || false + ); + + this.gdb.on("consoleStreamOutput", (output, category) => { + this.sendEvent(new OutputEvent(output, category)); + }); + + this.gdb.on("execAsync", (resultClass, resultData) => + this.handleGDBAsync(resultClass, resultData) + ); + this.gdb.on("notifyAsync", (resultClass, resultData) => + this.handleGDBNotify(resultClass, resultData) + ); + } + + protected async startGDBServer( + args: TargetLaunchRequestArguments + ): Promise { + if (args.target === undefined) { + args.target = {}; + } + const target = args.target; + const serverExe = target.server !== undefined ? target.server : "gdbserver"; + const serverCwd = target.cwd !== undefined ? target.cwd : getGdbCwd(args); + const serverParams = + target.serverParameters !== undefined + ? target.serverParameters + : ["--once", ":0", args.program]; + + this.killGdbServer = target.automaticallyKillServer !== false; + + const gdbEnvironment = args.environment + ? createEnvValues(process.env, args.environment) + : process.env; + const serverEnvironment = target.environment + ? createEnvValues(gdbEnvironment, target.environment) + : gdbEnvironment; + // Wait until gdbserver is started and ready to receive connections. + await new Promise((resolve, reject) => { + this.gdbserver = spawn(serverExe, serverParams, { + cwd: serverCwd, + env: serverEnvironment, + }); + let gdbserverStartupResolved = false; + let accumulatedStdout = ""; + let accumulatedStderr = ""; + let checkTargetPort = (_data: any) => { + // do nothing by default + }; + if (target.port && target.serverParameters) { + setTimeout( + () => { + gdbserverStartupResolved = true; + resolve(); + }, + target.serverStartupDelay !== undefined + ? target.serverStartupDelay + : 0 + ); + } else { + checkTargetPort = (data: any) => { + const regex = new RegExp( + target.serverPortRegExp + ? target.serverPortRegExp + : "Listening on port ([0-9]+)\r?\n" + ); + const m = regex.exec(data); + if (m !== null) { + target.port = m[1]; + checkTargetPort = (_data: any) => { + // do nothing now that we have our port + }; + setTimeout( + () => { + gdbserverStartupResolved = true; + resolve(); + }, + target.serverStartupDelay !== undefined + ? target.serverStartupDelay + : 0 + ); + } + }; + } + if (this.gdbserver.stdout) { + this.gdbserver.stdout.on("data", (data) => { + const out = data.toString(); + if (!gdbserverStartupResolved) { + accumulatedStdout += out; + } + this.sendEvent(new OutputEvent(out, "server")); + checkTargetPort(accumulatedStdout); + }); + } else { + throw new Error("Missing stdout in spawned gdbserver"); + } + + if (this.gdbserver.stderr) { + this.gdbserver.stderr.on("data", (data) => { + const err = data.toString(); + if (!gdbserverStartupResolved) { + accumulatedStderr += err; + } + this.sendEvent(new OutputEvent(err, "server")); + checkTargetPort(accumulatedStderr); + }); + } else { + throw new Error("Missing stderr in spawned gdbserver"); + } + + this.gdbserver.on("exit", (code, signal) => { + let exitmsg: string; + if (code === null) { + exitmsg = `${serverExe} is killed by signal ${signal}`; + } else { + exitmsg = `${serverExe} has exited with code ${code}`; + } + this.sendEvent(new OutputEvent(exitmsg, "server")); + if (!gdbserverStartupResolved) { + gdbserverStartupResolved = true; + reject(new Error(exitmsg + "\n" + accumulatedStderr)); + } + }); + + this.gdbserver.on("error", (err) => { + const errmsg = `${serverExe} has hit error ${err}`; + this.sendEvent(new OutputEvent(errmsg, "server")); + if (!gdbserverStartupResolved) { + gdbserverStartupResolved = true; + reject(new Error(errmsg + "\n" + accumulatedStderr)); + } + }); + }); + } + + protected initializeUARTConnection( + uart: UARTArguments, + host: string | undefined + ): void { + if (uart.serialPort !== undefined) { + // Set the path to the serial port + this.serialPort = new SerialPort({ + path: uart.serialPort, + // If the serial port path is defined, then so will the baud rate. + baudRate: uart.baudRate ?? 115200, + // If the serial port path is deifned, then so will the number of data bits. + dataBits: uart.characterSize ?? 8, + // If the serial port path is defined, then so will the number of stop bits. + stopBits: uart.stopBits ?? 1, + // If the serial port path is defined, then so will the parity check type. + parity: uart.parity ?? "none", + // If the serial port path is defined, then so will the type of handshaking method. + rtscts: uart.handshakingMethod === "RTS/CTS" ? true : false, + xon: uart.handshakingMethod === "XON/XOFF" ? true : false, + xoff: uart.handshakingMethod === "XON/XOFF" ? true : false, + autoOpen: false, + }); + + this.serialPort.on("open", () => { + this.sendEvent( + new OutputEvent( + `listening on serial port ${this.serialPort?.path}${os.EOL}`, + "Serial Port" + ) + ); + }); + + const SerialUartParser = new ReadlineParser({ + delimiter: uart.eolCharacter === "CRLF" ? "\r\n" : "\n", + encoding: "utf8", + }); + + this.serialPort.pipe(SerialUartParser).on("data", (line: string) => { + this.sendEvent(new OutputEvent(line + os.EOL, "Serial Port")); + }); + + this.serialPort.on("close", () => { + this.sendEvent( + new OutputEvent( + `closing serial port connection${os.EOL}`, + "Serial Port" + ) + ); + }); + + this.serialPort.on("error", (err) => { + this.sendEvent( + new OutputEvent( + `error on serial port connection${os.EOL} - ${err}`, + "Serial Port" + ) + ); + }); + + this.serialPort.open(); + } else if (uart.socketPort !== undefined) { + this.socket = new Socket(); + this.socket.setEncoding("utf-8"); + + let tcpUartData = ""; + this.socket.on("data", (data: string) => { + for (const char of data) { + if (char === "\n") { + this.sendEvent(new OutputEvent(tcpUartData + "\n", "Socket")); + tcpUartData = ""; + } else { + tcpUartData += char; + } + } + }); + this.socket.on("close", () => { + this.sendEvent(new OutputEvent(tcpUartData + os.EOL, "Socket")); + this.sendEvent( + new OutputEvent(`closing socket connection${os.EOL}`, "Socket") + ); + }); + this.socket.on("error", (err) => { + this.sendEvent( + new OutputEvent( + `error on socket connection${os.EOL} - ${err}`, + "Socket" + ) + ); + }); + this.socket.connect( + // Putting a + (unary plus operator) infront of the string converts it to a number. + +uart.socketPort, + // Default to localhost if target.host is undefined. + host ?? "localhost", + () => { + this.sendEvent( + new OutputEvent( + `listening on tcp port ${uart?.socketPort}${os.EOL}`, + "Socket" + ) + ); + } + ); + } + } + + protected async startGDBAndAttachToTarget( + response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse, + args: TargetAttachRequestArguments + ): Promise { + if (args.target === undefined) { + args.target = {}; + } + const target = args.target; + try { + this.isAttach = true; + await this.spawn(args); + await this.gdb.sendFileExecAndSymbols(args.program); + await this.gdb.sendEnablePrettyPrint(); + if (args.imageAndSymbols) { + if (args.imageAndSymbols.symbolFileName) { + if (args.imageAndSymbols.symbolOffset) { + await this.gdb.sendAddSymbolFile( + args.imageAndSymbols.symbolFileName, + args.imageAndSymbols.symbolOffset + ); + } else { + await this.gdb.sendFileSymbolFile( + args.imageAndSymbols.symbolFileName + ); + } + } + } + + if (target.connectCommands === undefined) { + this.targetType = target.type !== undefined ? target.type : "remote"; + let defaultTarget: string[]; + if (target.port !== undefined) { + defaultTarget = [ + target.host !== undefined + ? `${target.host}:${target.port}` + : `localhost:${target.port}`, + ]; + } else { + defaultTarget = []; + } + const targetParameters = + target.parameters !== undefined ? target.parameters : defaultTarget; + await mi.sendTargetSelectRequest(this.gdb, { + type: this.targetType, + parameters: targetParameters, + }); + this.sendEvent( + new OutputEvent( + `connected to ${this.targetType} target ${targetParameters.join( + " " + )}` + ) + ); + } else { + await this.gdb.sendCommands(target.connectCommands); + this.sendEvent( + new OutputEvent("connected to target using provided connectCommands") + ); + } + + await this.gdb.sendCommands(args.initCommands); + + if (target.uart !== undefined) { + this.initializeUARTConnection(target.uart, target.host); + } + + if (args.imageAndSymbols) { + if (args.imageAndSymbols.imageFileName) { + await this.gdb.sendLoad( + args.imageAndSymbols.imageFileName, + args.imageAndSymbols.imageOffset + ); + } + } + await this.gdb.sendCommands(args.preRunCommands); + this.sendEvent(new InitializedEvent()); + this.sendResponse(response); + this.isInitialized = true; + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } + + protected async stopGDBServer(): Promise { + return new Promise((resolve, reject) => { + if (!this.gdbserver || this.gdbserver.exitCode !== null) { + resolve(); + } else { + this.gdbserver.on("exit", () => { + resolve(); + }); + this.gdbserver?.kill(); + } + setTimeout(() => { + reject(); + }, 1000); + }); + } + + protected async disconnectRequest( + response: DebugProtocol.DisconnectResponse, + _args: DebugProtocol.DisconnectArguments + ): Promise { + try { + if (this.serialPort !== undefined && this.serialPort.isOpen) + this.serialPort.close(); + + if (this.targetType === "remote") { + if (this.gdb.getAsyncMode() && this.isRunning) { + // See #295 - this use of "then" is to try to slightly delay the + // call to disconnect. A proper solution that waits for the + // interrupt to be successful is needed to avoid future + // "Cannot execute this command while the target is running" + // errors + this.gdb + .sendCommand("interrupt") + .then(() => this.gdb.sendCommand("disconnect")); + } else { + await this.gdb.sendCommand("disconnect"); + } + } + + await this.gdb.sendGDBExit(); + if (this.killGdbServer) { + await this.stopGDBServer(); + this.sendEvent(new OutputEvent("gdbserver stopped", "server")); + } + this.sendResponse(response); + } catch (err) { + this.sendErrorResponse( + response, + 1, + err instanceof Error ? err.message : String(err) + ); + } + } +} diff --git a/src/cdtDebugAdapter/adapter/LICENSE b/src/cdtDebugAdapter/adapter/LICENSE new file mode 100644 index 000000000..d3087e4c5 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/src/cdtDebugAdapter/adapter/MIParser.ts b/src/cdtDebugAdapter/adapter/MIParser.ts new file mode 100644 index 000000000..4e2f060d1 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/MIParser.ts @@ -0,0 +1,380 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { Readable } from 'stream'; +import { logger } from '@vscode/debugadapter/lib/logger'; +import { GDBBackend } from './GDBBackend'; +import * as utf8 from 'utf8'; + +type CommandQueue = { + [key: string]: (resultClass: string, resultData: any) => void; +}; + +export class MIParser { + protected line = ''; + protected pos = 0; + + protected commandQueue: CommandQueue = {}; + protected waitReady?: (value?: void | PromiseLike) => void; + + constructor(protected gdb: GDBBackend) {} + + public parse(stream: Readable): Promise { + return new Promise((resolve) => { + this.waitReady = resolve; + const lineBreakRegex = /\r?\n/; + let buff = ''; + stream.on('data', (chunk) => { + const newChunk = chunk.toString(); + let regexArray = lineBreakRegex.exec(newChunk); + if (regexArray) { + regexArray.index += buff.length; + } + buff += newChunk; + while (regexArray) { + const line = buff.slice(0, regexArray.index); + this.parseLine(line); + buff = buff.slice(regexArray.index + regexArray[0].length); + regexArray = lineBreakRegex.exec(buff); + } + }); + }); + } + + public parseLine(line: string) { + this.line = line; + this.pos = 0; + this.handleLine(); + } + + public queueCommand( + token: number, + command: (resultClass: string, resultData: any) => void + ) { + this.commandQueue[token] = command; + } + + protected peek() { + if (this.pos < this.line.length) { + return this.line[this.pos]; + } else { + return null; + } + } + + protected next() { + if (this.pos < this.line.length) { + return this.line[this.pos++]; + } else { + return null; + } + } + + protected back() { + this.pos--; + } + + protected restOfLine() { + return this.line.substr(this.pos); + } + + protected handleToken(firstChar: string) { + let token = firstChar; + let c = this.next(); + while (c && c >= '0' && c <= '9') { + token += c; + c = this.next(); + } + this.back(); + return token; + } + + protected handleCString() { + let c = this.next(); + if (!c || c !== '"') { + return null; + } + + let cstring = ''; + let octal = ''; + mainloop: for (c = this.next(); c; c = this.next()) { + if (octal) { + octal += c; + if (octal.length == 3) { + cstring += String.fromCodePoint(parseInt(octal, 8)); + octal = ''; + } + continue; + } + switch (c) { + case '"': + break mainloop; + case '\\': + c = this.next(); + if (c) { + switch (c) { + case 'n': + cstring += '\n'; + break; + case 't': + cstring += '\t'; + break; + case 'r': + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + octal = c; + break; + default: + cstring += c; + } + } else { + this.back(); + } + break; + default: + cstring += c; + } + } + + try { + return utf8.decode(cstring); + } catch (err) { + logger.error( + `Failed to decode cstring '${cstring}'. ${JSON.stringify(err)}` + ); + return cstring; + } + } + + protected handleString() { + let str = ''; + for (let c = this.next(); c; c = this.next()) { + if (c === '=' || c === ',') { + this.back(); + return str; + } else { + str += c; + } + } + return str; + } + + protected handleObject() { + let c = this.next(); + const result: any = {}; + if (c === '{') { + c = this.next(); + if (c !== '"') { + // oject contains name-value pairs + while (c !== '}') { + if (c !== ',') { + this.back(); + } + const name = this.handleString(); + if (this.next() === '=') { + result[name] = this.handleValue(); + } + c = this.next(); + } + } else { + // "object" contains just values + this.back(); + let key = 0; + while (c !== '}') { + let value = this.handleCString(); + if (value) result[key++] = value; + c = this.next(); + } + } + } + + if (c === '}') { + return result; + } else { + return null; + } + } + + protected handleArray() { + let c = this.next(); + const result: any[] = []; + if (c === '[') { + c = this.next(); + while (c !== ']') { + if (c !== ',') { + this.back(); + } + result.push(this.handleValue()); + c = this.next(); + } + } + + if (c === ']') { + return result; + } else { + return null; + } + } + + protected handleValue(): any { + const c = this.next(); + this.back(); + switch (c) { + case '"': + return this.handleCString(); + case '{': + return this.handleObject(); + case '[': + return this.handleArray(); + default: + // A weird array element with a name, ignore the name and return the value + this.handleString(); + if (this.next() === '=') { + return this.handleValue(); + } + } + return null; + } + + protected handleAsyncData() { + const result: any = {}; + + let c = this.next(); + let name = 'missing'; + while (c === ',') { + if (this.peek() !== '{') { + name = this.handleString(); + if (this.next() === '=') { + result[name] = this.handleValue(); + } + } else { + // In some cases, such as -break-insert with multiple results + // GDB does not return an array, so we have to identify that + // case and convert result to an array + // An example is (many fields removed to make example readable): + // 3-break-insert --function staticfunc1 + // 3^done,bkpt={number="1",addr=""},{number="1.1",func="staticfunc1",file="functions.c"},{number="1.2",func="staticfunc1",file="functions_other.c"} + if (!Array.isArray(result[name])) { + result[name] = [result[name]]; + } + result[name].push(this.handleValue()); + } + c = this.next(); + } + + return result; + } + + protected handleConsoleStream() { + const msg = this.handleCString(); + if (msg) { + this.gdb.emit('consoleStreamOutput', msg, 'stdout'); + } + } + + protected handleLogStream() { + const msg = this.handleCString(); + if (msg) { + this.gdb.emit('consoleStreamOutput', msg, 'log'); + } + } + + protected handleLine() { + let c = this.next(); + if (!c) { + return; + } + + let token = ''; + + if (c >= '0' && c <= '9') { + token = this.handleToken(c); + c = this.next(); + } + + switch (c) { + case '^': { + const rest = this.restOfLine(); + for (let i = 0; i < rest.length; i += 1000) { + const msg = i === 0 ? 'result' : '-cont-'; + logger.verbose( + `GDB ${msg}: ${token} ${rest.substr(i, 1000)}` + ); + } + const command = this.commandQueue[token]; + if (command) { + const resultClass = this.handleString(); + const resultData = this.handleAsyncData(); + command(resultClass, resultData); + delete this.commandQueue[token]; + } else { + logger.error('GDB response with no command: ' + token); + } + break; + } + case '~': + case '@': + this.handleConsoleStream(); + break; + case '&': + this.handleLogStream(); + break; + case '=': { + logger.verbose('GDB notify async: ' + this.restOfLine()); + const notifyClass = this.handleString(); + this.gdb.emit( + 'notifyAsync', + notifyClass, + this.handleAsyncData() + ); + break; + } + case '*': { + logger.verbose('GDB exec async: ' + this.restOfLine()); + const execClass = this.handleString(); + this.gdb.emit('execAsync', execClass, this.handleAsyncData()); + break; + } + case '+': { + logger.verbose('GDB status async: ' + this.restOfLine()); + const statusClass = this.handleString(); + this.gdb.emit( + 'statusAsync', + statusClass, + this.handleAsyncData() + ); + break; + } + case '(': + // this is the (gdb) prompt and used + // to know that GDB has started and is ready + // for commands + if (this.waitReady) { + this.waitReady(); + this.waitReady = undefined; + } + break; + default: + // treat as console output. happens on Windows. + this.back(); + this.gdb.emit( + 'consoleStreamOutput', + this.restOfLine() + '\n', + 'stdout' + ); + } + } +} diff --git a/src/cdtDebugAdapter/adapter/NOTICE b/src/cdtDebugAdapter/adapter/NOTICE new file mode 100644 index 000000000..8c2e6b8b0 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/NOTICE @@ -0,0 +1,48 @@ +# Notices for Eclipse CDT.cloud + +This content is produced and maintained by the Eclipse CDT.cloud project. + +* Project home: https://projects.eclipse.org/projects/ecd.cdt.cloud + +## Trademarks + + Eclipse CDT.cloud is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v. 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0. + +SPDX-License-Identifier: EPL-2.0 + +## Source Code + +The project maintains the following source code repositories: + +* https://github.com/eclipse-cdt-cloud/cdt-cloud +* https://github.com/eclipse-cdt-cloud/cdt-cloud-blueprint +* https://github.com/eclipse-cdt-cloud/cdt-amalgamator +* https://github.com/eclipse-cdt-cloud/cdt-gdb-vscode +* https://github.com/eclipse-cdt-cloud/cdt-gdb-adapter + +## Third-party Content + +This project leverages the following third party content. + +None + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. diff --git a/src/cdtDebugAdapter/adapter/debugAdapter.ts b/src/cdtDebugAdapter/adapter/debugAdapter.ts new file mode 100644 index 000000000..9da3486f8 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/debugAdapter.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { logger } from '@vscode/debugadapter/lib/logger'; +import { GDBDebugSession } from './GDBDebugSession'; + +process.on('uncaughtException', (err: any) => { + logger.error(JSON.stringify(err)); +}); + +GDBDebugSession.run(GDBDebugSession); diff --git a/src/cdtDebugAdapter/adapter/debugTargetAdapter.ts b/src/cdtDebugAdapter/adapter/debugTargetAdapter.ts new file mode 100644 index 000000000..a9ba08f5b --- /dev/null +++ b/src/cdtDebugAdapter/adapter/debugTargetAdapter.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { logger } from '@vscode/debugadapter/lib/logger'; +import { GDBTargetDebugSession } from './GDBTargetDebugSession'; + +process.on('uncaughtException', (err: any) => { + logger.error(JSON.stringify(err)); +}); + +GDBTargetDebugSession.run(GDBTargetDebugSession); diff --git a/src/cdtDebugAdapter/adapter/index.ts b/src/cdtDebugAdapter/adapter/index.ts new file mode 100644 index 000000000..f0db3a82d --- /dev/null +++ b/src/cdtDebugAdapter/adapter/index.ts @@ -0,0 +1,14 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ + +export * from './mi'; +export * from './GDBBackend'; +export * from './GDBDebugSession'; +export * from './GDBTargetDebugSession'; diff --git a/src/cdtDebugAdapter/adapter/mi/base.ts b/src/cdtDebugAdapter/adapter/mi/base.ts new file mode 100644 index 000000000..5508c8a31 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/base.ts @@ -0,0 +1,88 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; + +export interface MIResponse { + _class: string; +} + +export abstract class MIRequest { + public abstract send(backend: GDBBackend): Promise; +} + +// Shared types +/** See {@link https://sourceware.org/gdb/current/onlinedocs/gdb.html/GDB_002fMI-Breakpoint-Information.html this documentation} for additional details. */ +export interface MIBreakpointInfo { + disp: string; + enabled: 'y' | 'n'; + number: string; + type: string; + addr?: string; + addr_flags?: string; + at?: string; + 'catch-type'?: string; + cond?: string; + enable?: string; + 'evaluated-by'?: 'host' | 'target'; + file?: string; // docs say filname, but that is wrong + frame?: string; + fullname?: string; + func?: string; + ignore?: string; + inferior?: string; + installed?: 'y' | 'n'; + line?: string; + locations?: MILocation[]; + mask?: string; + 'original-location'?: string; + pass?: string; + pending?: string; + script?: string; + task?: string; + thread?: string; + 'thread-groups'?: string[]; + times: string; + what?: string; + // TODO there are a few more fields here +} + +/** See {@link https://sourceware.org/gdb/current/onlinedocs/gdb.html/GDB_002fMI-Breakpoint-Information.html this documentation} for additional details. */ +export interface MILocation { + number: string; + enabled: 'y' | 'n' | 'N'; + addr: string; + addr_flags?: string; + func?: string; + file?: string; + fullname?: string; + line?: string; + 'thread-groups': string[]; +} + +export interface MIFrameInfo { + level: string; + func?: string; + addr?: string; + file?: string; + fullname?: string; + line?: string; + from?: string; +} + +export interface MIVariableInfo { + name: string; + value?: string; + type?: string; +} + +export interface MIRegisterValueInfo { + number: string; + value: string; +} diff --git a/src/cdtDebugAdapter/adapter/mi/breakpoint.ts b/src/cdtDebugAdapter/adapter/mi/breakpoint.ts new file mode 100644 index 000000000..2308974c9 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/breakpoint.ts @@ -0,0 +1,176 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +import { MIBreakpointInfo, MIResponse } from './base'; + +/** + * The generic MI Parser (see MIParser.handleAsyncData) cannot differentiate + * properly between an array or single result from -break-insert. Therefore + * we get two possible response types. The cleanupBreakpointResponse + * normalizes the response. + */ +interface MIBreakInsertResponseInternal extends MIResponse { + bkpt: MIBreakpointInfo[] | MIBreakpointInfo; +} +export interface MIBreakInsertResponse extends MIResponse { + bkpt: MIBreakpointInfo; + /** + * In cases where GDB inserts multiple breakpoints, the "children" + * breakpoints will be stored in multiple field. + */ + multiple?: MIBreakpointInfo[]; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MIBreakDeleteRequest {} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MIBreakDeleteResponse extends MIResponse {} + +export interface MIBreakListResponse extends MIResponse { + BreakpointTable: { + nr_rows: string; + nr_cols: string; + hrd: Array<{ + width: string; + alignment: string; + col_name: string; + colhdr: string; + }>; + body: MIBreakpointInfo[]; + }; +} + +export interface MIBreakpointInsertOptions { + temporary?: boolean; + hardware?: boolean; + pending?: boolean; + disabled?: boolean; + tracepoint?: boolean; + condition?: string; + ignoreCount?: number; + threadId?: string; +} + +export interface MIBreakpointLocation { + locationType?: 'source' | 'function'; + source?: string; + line?: string; + fn?: string; +} + +function cleanupBreakpointResponse( + raw: MIBreakInsertResponseInternal +): MIBreakInsertResponse { + if (Array.isArray(raw.bkpt)) { + const bkpt = raw.bkpt[0]; + const multiple = raw.bkpt.slice(1); + return { + _class: raw._class, + bkpt, + multiple, + }; + } + return { + _class: raw._class, + bkpt: raw.bkpt, + }; +} + +export function sourceBreakpointLocation( + gdb: GDBBackend, + source: string, + line = '', + forInsert = false +): string { + const version8 = gdb.gdbVersionAtLeast('8.0'); + if (forInsert) { + if (version8) { + return `--source ${gdb.standardEscape(source)} --line ${line}`; + } else { + // double-escaping/quoting needed for old GDBs + const location = `"${source}:${line}"`; + return `${gdb.standardEscape(location, true)}`; + } + } else { + return version8 + ? `-source ${source} -line ${line}` + : `${source}:${line}`; + } +} + +export function functionBreakpointLocation( + gdb: GDBBackend, + fn: string, + forInsert = false +): string { + const version8 = gdb.gdbVersionAtLeast('8.0'); + if (forInsert) { + return version8 ? `--function ${fn}` : fn; + } else { + return version8 ? `-function ${fn}` : fn; + } +} + +export async function sendBreakpointInsert( + gdb: GDBBackend, + location: string, + options?: MIBreakpointInsertOptions +): Promise { + // Todo: lots of options + const temp = options?.temporary ? '-t ' : ''; + const ignore = options?.ignoreCount ? `-i ${options?.ignoreCount} ` : ''; + const hwBreakpoint = options?.hardware ? '-h ' : ''; + const pend = options?.pending ? '-f ' : ''; + const command = `-break-insert ${temp}${hwBreakpoint}${ignore}${pend}${location}`; + const result = await gdb.sendCommand( + command + ); + const clean = cleanupBreakpointResponse(result); + if (options?.condition) { + await gdb.sendCommand( + `-break-condition ${clean.bkpt.number} ${options.condition}` + ); + } + + return clean; +} + +export function sendBreakDelete( + gdb: GDBBackend, + request: { + breakpoints: string[]; + } +): Promise { + return gdb.sendCommand(`-break-delete ${request.breakpoints.join(' ')}`); +} + +export function sendBreakList(gdb: GDBBackend): Promise { + return gdb.sendCommand('-break-list'); +} + +export async function sendFunctionBreakpointInsert( + gdb: GDBBackend, + fn: string, + options?: MIBreakpointInsertOptions +): Promise { + const location = await functionBreakpointLocation(gdb, fn, true); + return sendBreakpointInsert(gdb, location, options); +} + +export async function sendSourceBreakpointInsert( + gdb: GDBBackend, + source: string, + line?: string, + options?: MIBreakpointInsertOptions +): Promise { + const location = await sourceBreakpointLocation(gdb, source, line, true); + return sendBreakpointInsert(gdb, location, options); +} diff --git a/src/cdtDebugAdapter/adapter/mi/data.ts b/src/cdtDebugAdapter/adapter/mi/data.ts new file mode 100644 index 000000000..a7166c08f --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/data.ts @@ -0,0 +1,155 @@ +/********************************************************************* + * Copyright (c) 2018 Ericsson and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ + +import { GDBBackend } from '../GDBBackend'; +import { MIResponse, MIRegisterValueInfo } from './base'; + +interface MIDataReadMemoryBytesResponse { + memory: Array<{ + begin: string; + end: string; + offset: string; + contents: string; + }>; +} +interface MIDataDisassembleAsmInsn { + address: string; + // func-name in MI + 'func-name': string; + offset: string; + opcodes: string; + inst: string; +} + +interface MIDataDisassembleSrcAndAsmLine { + line: string; + file: string; + fullname: string; + line_asm_insn: MIDataDisassembleAsmInsn[]; +} +interface MIDataDisassembleResponse { + asm_insns: MIDataDisassembleSrcAndAsmLine[]; +} + +export interface MIListRegisterNamesResponse extends MIResponse { + 'register-names': string[]; +} + +export interface MIListRegisterValuesResponse extends MIResponse { + 'register-values': MIRegisterValueInfo[]; +} + +export interface MIGDBDataEvaluateExpressionResponse extends MIResponse { + value?: string; +} + +export function sendDataReadMemoryBytes( + gdb: GDBBackend, + address: string, + size: number, + offset = 0 +): Promise { + return gdb.sendCommand( + `-data-read-memory-bytes -o ${offset} "${address}" ${size}` + ); +} + +export function sendDataWriteMemoryBytes( + gdb: GDBBackend, + memoryReference: string, + data: string +): Promise { + return gdb.sendCommand( + `-data-write-memory-bytes "${memoryReference}" "${data}"` + ); +} + +export function sendDataEvaluateExpression( + gdb: GDBBackend, + expr: string +): Promise { + return gdb.sendCommand(`-data-evaluate-expression "${expr}"`); +} + +// https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI-Data-Manipulation.html#The-_002ddata_002ddisassemble-Command +export async function sendDataDisassemble( + gdb: GDBBackend, + startAddress: string, + endAddress: string +): Promise { + // -- 5 == mixed source and disassembly with raw opcodes + // needs to be deprecated mode 3 for GDB < 7.11 + const mode = gdb.gdbVersionAtLeast('7.11') ? '5' : '3'; + const result: MIDataDisassembleResponse = await gdb.sendCommand( + `-data-disassemble -s "${startAddress}" -e "${endAddress}" -- ${mode}` + ); + + // cleanup the result data + if (result.asm_insns.length > 0) { + if ( + !Object.prototype.hasOwnProperty.call( + result.asm_insns[0], + 'line_asm_insn' + ) + ) { + // In this case there is no source info available for any instruction, + // so GDB treats as if we had done -- 2 instead of -- 5 + // This bit of code remaps the data to look like it should + const e: MIDataDisassembleSrcAndAsmLine = { + line_asm_insn: + result.asm_insns as unknown as MIDataDisassembleAsmInsn[], + } as MIDataDisassembleSrcAndAsmLine; + result.asm_insns = [e]; + } + for (const asmInsn of result.asm_insns) { + if ( + !Object.prototype.hasOwnProperty.call(asmInsn, 'line_asm_insn') + ) { + asmInsn.line_asm_insn = []; + } + } + } + return Promise.resolve(result); +} + +export function sendDataListRegisterNames( + gdb: GDBBackend, + params: { + regno?: number[]; + frameId: number; + threadId: number; + } +): Promise { + let command = `-data-list-register-names --frame ${params.frameId} --thread ${params.threadId}`; + + if (params.regno) { + command += params.regno.join(' '); + } + + return gdb.sendCommand(command); +} + +export function sendDataListRegisterValues( + gdb: GDBBackend, + params: { + fmt: string; + regno?: number[]; + frameId: number; + threadId: number; + } +): Promise { + let command = `-data-list-register-values --frame ${params.frameId} --thread ${params.threadId} ${params.fmt}`; + + if (params.regno) { + command += params.regno.join(' '); + } + + return gdb.sendCommand(command); +} diff --git a/src/cdtDebugAdapter/adapter/mi/exec.ts b/src/cdtDebugAdapter/adapter/mi/exec.ts new file mode 100644 index 000000000..029f2ab52 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/exec.ts @@ -0,0 +1,84 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +import { MIResponse } from './base'; + +export function sendExecArguments( + gdb: GDBBackend, + params: { + arguments: string; + } +): Promise { + return gdb.sendCommand(`-exec-arguments ${params.arguments}`); +} + +export function sendExecRun(gdb: GDBBackend) { + return gdb.sendCommand('-exec-run'); +} + +export function sendExecContinue(gdb: GDBBackend, threadId?: number) { + let command = '-exec-continue'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendExecNext(gdb: GDBBackend, threadId?: number) { + let command = '-exec-next'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendExecNextInstruction(gdb: GDBBackend, threadId?: number) { + let command = '-exec-next-instruction'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendExecStep(gdb: GDBBackend, threadId?: number) { + let command = '-exec-step'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendExecStepInstruction(gdb: GDBBackend, threadId?: number) { + let command = '-exec-step-instruction'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendExecFinish(gdb: GDBBackend, threadId?: number) { + let command = '-exec-finish'; + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } + return gdb.sendCommand(command); +} + +export function sendExecInterrupt(gdb: GDBBackend, threadId?: number) { + let command = '-exec-interrupt'; + + if (threadId !== undefined) { + command += ` --thread ${threadId}`; + } else { + command += ' --all'; + } + + return gdb.sendCommand(command); +} diff --git a/src/cdtDebugAdapter/adapter/mi/index.ts b/src/cdtDebugAdapter/adapter/mi/index.ts new file mode 100644 index 000000000..b672c9186 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/index.ts @@ -0,0 +1,18 @@ +/********************************************************************* + * Copyright (c) 2018 Ericsson and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +export * from './base'; +export * from './breakpoint'; +export * from './data'; +export * from './exec'; +export * from './stack'; +export * from './target'; +export * from './thread'; +export * from './var'; +export * from './interpreter'; diff --git a/src/cdtDebugAdapter/adapter/mi/interpreter.ts b/src/cdtDebugAdapter/adapter/mi/interpreter.ts new file mode 100644 index 000000000..a7f484589 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/interpreter.ts @@ -0,0 +1,22 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +export function sendInterpreterExecConsole( + gdb: GDBBackend, + params: { + threadId: number; + frameId: number; + command: any; + } +) { + return gdb.sendCommand( + `-interpreter-exec --thread ${params.threadId} --frame ${params.frameId} console "${params.command}"` + ); +} diff --git a/src/cdtDebugAdapter/adapter/mi/stack.ts b/src/cdtDebugAdapter/adapter/mi/stack.ts new file mode 100644 index 000000000..2a17cb740 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/stack.ts @@ -0,0 +1,100 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +import { MIFrameInfo, MIResponse, MIVariableInfo } from './base'; + +export interface MIStackInfoDepthResponse extends MIResponse { + depth: string; +} + +export interface MIStackListVariablesResponse extends MIResponse { + variables: MIVariableInfo[]; +} + +export function sendStackInfoDepth( + gdb: GDBBackend, + params: { + maxDepth: number; + threadId?: number; + } +): Promise { + let command = '-stack-info-depth'; + if (params.threadId !== undefined) { + command += ` --thread ${params.threadId}`; + } + if (params.maxDepth) { + command += ` ${params.maxDepth}`; + } + return gdb.sendCommand(command); +} + +export function sendStackListFramesRequest( + gdb: GDBBackend, + params: { + noFrameFilters?: boolean; + lowFrame?: number; + highFrame?: number; + threadId?: number; + } +): Promise<{ + stack: MIFrameInfo[]; +}> { + let command = '-stack-list-frames'; + if (params.threadId !== undefined) { + command += ` --thread ${params.threadId}`; + } + if (params.noFrameFilters) { + command += ' -no-frame-filters'; + } + if (params.lowFrame !== undefined) { + command += ` ${params.lowFrame}`; + } + if (params.highFrame !== undefined) { + command += ` ${params.highFrame}`; + } + return gdb.sendCommand(command); +} + +export function sendStackSelectFrame( + gdb: GDBBackend, + params: { + framenum: number; + } +): Promise { + return gdb.sendCommand(`-stack-select-frame ${params.framenum}`); +} + +export function sendStackListVariables( + gdb: GDBBackend, + params: { + thread?: number; + frame?: number; + printValues: 'no-values' | 'all-values' | 'simple-values'; + noFrameFilters?: boolean; + skipUnavailable?: boolean; + } +): Promise { + let command = '-stack-list-variables'; + if (params.noFrameFilters) { + command += ' --no-frame-filters'; + } + if (params.skipUnavailable) { + command += ' --skip-unavailable'; + } + if (params.thread !== undefined) { + command += ` --thread ${params.thread}`; + } + if (params.frame !== undefined) { + command += ` --frame ${params.frame}`; + } + command += ` --${params.printValues}`; + + return gdb.sendCommand(command); +} diff --git a/src/cdtDebugAdapter/adapter/mi/target.ts b/src/cdtDebugAdapter/adapter/mi/target.ts new file mode 100644 index 000000000..cc4cb4044 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/target.ts @@ -0,0 +1,32 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +import { MIResponse } from './base'; + +export function sendTargetAttachRequest( + gdb: GDBBackend, + params: { + pid: string; + } +): Promise { + return gdb.sendCommand(`-target-attach ${params.pid}`); +} + +export function sendTargetSelectRequest( + gdb: GDBBackend, + params: { + type: string; + parameters: string[]; + } +): Promise { + return gdb.sendCommand( + `-target-select ${params.type} ${params.parameters.join(' ')}` + ); +} diff --git a/src/cdtDebugAdapter/adapter/mi/thread.ts b/src/cdtDebugAdapter/adapter/mi/thread.ts new file mode 100644 index 000000000..896df0c48 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/thread.ts @@ -0,0 +1,53 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +import { MIFrameInfo, MIResponse } from './base'; + +export interface MIThreadInfo { + id: string; + targetId: string; + details?: string; + name?: string; + state: string; + frame?: MIFrameInfo; + core?: string; +} + +export interface MIThreadInfoResponse extends MIResponse { + threads: MIThreadInfo[]; + 'current-thread-id': string; +} + +export function sendThreadInfoRequest( + gdb: GDBBackend, + params: { + threadId?: string; + } +): Promise { + let command = '-thread-info'; + if (params.threadId) { + command += ` ${params.threadId}`; + } + return gdb.sendCommand(command); +} + +export interface MIThreadSelectResponse extends MIResponse { + 'new-thread-id': string; + frame: MIFrameInfo; +} + +export function sendThreadSelectRequest( + gdb: GDBBackend, + params: { + threadId: number; + } +): Promise { + return gdb.sendCommand(`-thread-select ${params.threadId}`); +} diff --git a/src/cdtDebugAdapter/adapter/mi/var.ts b/src/cdtDebugAdapter/adapter/mi/var.ts new file mode 100644 index 000000000..d4f7d307d --- /dev/null +++ b/src/cdtDebugAdapter/adapter/mi/var.ts @@ -0,0 +1,206 @@ +/********************************************************************* + * Copyright (c) 2018 QNX Software Systems and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { GDBBackend } from '../GDBBackend'; +import { MIResponse } from './base'; + +export enum MIVarPrintValues { + no = '0', + all = '1', + simple = '2', +} + +export interface MIVarCreateResponse extends MIResponse { + name: string; + numchild: string; + value: string; + type: string; + 'thread-id'?: string; + has_more?: string; + dynamic?: string; + displayhint?: string; +} + +export interface MIVarListChildrenResponse { + numchild: string; + children: MIVarChild[]; +} + +export interface MIVarChild { + name: string; + exp: string; + numchild: string; + type: string; + value?: string; + 'thread-id'?: string; + frozen?: string; + displayhint?: string; + dynamic?: string; +} + +export interface MIVarUpdateResponse { + changelist: Array<{ + name: string; + value: string; + in_scope: string; + type_changed: string; + has_more: string; + }>; +} + +export interface MIVarEvalResponse { + value: string; +} + +export interface MIVarAssignResponse { + value: string; +} + +export interface MIVarPathInfoResponse { + path_expr: string; +} + +function quote(expression: string) { + return `"${expression}"`; +} + +export function sendVarCreate( + gdb: GDBBackend, + params: { + name?: string; + frameAddr?: string; + frame?: 'current' | 'floating'; + expression: string; + threadId?: number; + frameId?: number; + } +): Promise { + let command = '-var-create'; + if (params.threadId !== undefined) { + command += ` --thread ${params.threadId}`; + } + if (params.frameId !== undefined) { + command += ` --frame ${params.frameId}`; + } + + command += ` ${params.name ? params.name : '-'}`; + if (params.frameAddr) { + command += ` ${params.frameAddr}`; + } else if (params.frame) { + switch (params.frame) { + default: + case 'current': + command += ' *'; + break; + case 'floating': + command += ' @'; + break; + } + } else { + command += ' *'; + } + command += ` ${quote(params.expression)}`; + + return gdb.sendCommand(command); +} + +export function sendVarListChildren( + gdb: GDBBackend, + params: { + printValues?: + | MIVarPrintValues.no + | MIVarPrintValues.all + | MIVarPrintValues.simple; + name: string; + from?: number; + to?: number; + } +): Promise { + let command = '-var-list-children'; + if (params.printValues) { + command += ` ${params.printValues}`; + } + command += ` ${params.name}`; + if (params.from && params.to) { + command += ` ${params.from} ${params.to}`; + } + + return gdb.sendCommand(command); +} + +export function sendVarUpdate( + gdb: GDBBackend, + params: { + name?: string; + printValues?: + | MIVarPrintValues.no + | MIVarPrintValues.all + | MIVarPrintValues.simple; + } +): Promise { + let command = '-var-update'; + if (params.printValues) { + command += ` ${params.printValues}`; + } else { + command += ` ${MIVarPrintValues.all}`; + } + if (params.name) { + command += ` ${params.name}`; + } else { + command += ' *'; + } + return gdb.sendCommand(command); +} + +export function sendVarDelete( + gdb: GDBBackend, + params: { + varname: string; + } +): Promise { + const command = `-var-delete ${params.varname}`; + return gdb.sendCommand(command); +} + +export function sendVarAssign( + gdb: GDBBackend, + params: { + varname: string; + expression: string; + } +): Promise { + const command = `-var-assign ${params.varname} ${params.expression}`; + return gdb.sendCommand(command); +} + +export function sendVarEvaluateExpression( + gdb: GDBBackend, + params: { + varname: string; + } +): Promise { + const command = `-var-evaluate-expression ${params.varname}`; + return gdb.sendCommand(command); +} + +export function sendVarInfoPathExpression( + gdb: GDBBackend, + name: string +): Promise { + const command = `-var-info-path-expression ${name}`; + return gdb.sendCommand(command); +} + +export function sendVarSetFormatToHex( + gdb: GDBBackend, + name: string +): Promise { + const command = `-var-set-format ${name} hexadecimal`; + return gdb.sendCommand(command); +} diff --git a/src/cdtDebugAdapter/adapter/stoppedEvent.ts b/src/cdtDebugAdapter/adapter/stoppedEvent.ts new file mode 100644 index 000000000..d9a915874 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/stoppedEvent.ts @@ -0,0 +1,32 @@ +/********************************************************************* + * Copyright (c) 2019 Arm Ltd. and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { Event } from '@vscode/debugadapter'; +import { DebugProtocol } from '@vscode/debugprotocol'; + +export class StoppedEvent extends Event implements DebugProtocol.StoppedEvent { + public body: { + reason: string; + threadId?: number; + allThreadsStopped?: boolean; + }; + + constructor(reason: string, threadId: number, allThreadsStopped = false) { + super('stopped'); + + this.body = { + reason, + allThreadsStopped, + }; + + if (typeof threadId === 'number') { + this.body.threadId = threadId; + } + } +} diff --git a/src/cdtDebugAdapter/adapter/util.ts b/src/cdtDebugAdapter/adapter/util.ts new file mode 100644 index 000000000..8079cf574 --- /dev/null +++ b/src/cdtDebugAdapter/adapter/util.ts @@ -0,0 +1,182 @@ +/********************************************************************* + * Copyright (c) 2022 Kichwa Coders Canada, Inc. and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *********************************************************************/ +import { execFile } from 'child_process'; +import { platform } from 'os'; +import { promisify } from 'util'; +import { dirname } from 'path'; +import { existsSync } from 'fs'; + +/** + * This method actually launches 'gdb --version' to determine the version of + * the GDB that is being used. + * + * @param gdbPath the path to the GDB executable to be called + * @return the detected version of GDB at gdbPath + */ +export async function getGdbVersion( + gdbPath: string, + gdbCwd?: string, + environment?: Record +): Promise { + const gdbEnvironment = environment + ? createEnvValues(process.env, environment) + : process.env; + const { stdout, stderr } = await promisify(execFile)( + gdbPath, + ['--version'], + { cwd: gdbCwd, env: gdbEnvironment } + ); + + const gdbVersion = parseGdbVersionOutput(stdout); + if (!gdbVersion) { + throw new Error( + `Failed to get version number from GDB. GDB returned:\nstdout:\n${stdout}\nstderr:\n${stderr}` + ); + } + return gdbVersion; +} + +/** + * Find gdb version info from a string object which is supposed to + * contain output text of "gdb --version" command. + * + * @param stdout + * output text from "gdb --version" command . + * @return + * String representation of version of gdb such as "10.1" on success + */ +export function parseGdbVersionOutput(stdout: string): string | undefined { + return stdout.split(/ gdb( \(.*?\))? (\D* )*\(?(\d*(\.\d*)*)/g)[3]; +} + +/** + * Compares two version numbers. + * Returns -1, 0, or 1 if v1 is less than, equal to, or greater than v2, respectively. + * @param v1 The first version + * @param v2 The second version + * @return -1, 0, or 1 if v1 is less than, equal to, or greater than v2, respectively. + */ +export function compareVersions(v1: string, v2: string): number { + const v1Parts = v1.split(/\./); + const v2Parts = v2.split(/\./); + for (let i = 0; i < v1Parts.length && i < v2Parts.length; i++) { + const v1PartValue = parseInt(v1Parts[i], 10); + const v2PartValue = parseInt(v2Parts[i], 10); + + if (isNaN(v1PartValue) || isNaN(v2PartValue)) { + // Non-integer part, ignore it + continue; + } + if (v1PartValue > v2PartValue) { + return 1; + } else if (v1PartValue < v2PartValue) { + return -1; + } + } + + // If we get here is means the versions are still equal + // but there could be extra parts to examine + + if (v1Parts.length < v2Parts.length) { + // v2 has extra parts, which implies v1 is a lower version (e.g., v1 = 7.9 v2 = 7.9.1) + // unless each extra part is 0, in which case the two versions are equal (e.g., v1 = 7.9 v2 = 7.9.0) + for (let i = v1Parts.length; i < v2Parts.length; i++) { + const v2PartValue = parseInt(v2Parts[i], 10); + + if (isNaN(v2PartValue)) { + // Non-integer part, ignore it + continue; + } + if (v2PartValue != 0) { + return -1; + } + } + } + if (v1Parts.length > v2Parts.length) { + // v1 has extra parts, which implies v1 is a higher version (e.g., v1 = 7.9.1 v2 = 7.9) + // unless each extra part is 0, in which case the two versions are equal (e.g., v1 = 7.9.0 v2 = 7.9) + for (let i = v2Parts.length; i < v1Parts.length; i++) { + const v1PartValue = parseInt(v1Parts[i], 10); + + if (isNaN(v1PartValue)) { + // Non-integer part, ignore it + continue; + } + if (v1PartValue != 0) { + return 1; + } + } + } + + return 0; +} + +/** + * This method is providing an automatic operation to including new variables to process.env. + * Method is not injecting the new variables to current thread, rather it is returning a new + * object with included parameters. + * + * @param source + * Source environment variables to include. + * @param valuesToMerge + * Key-Value dictionary to include. + * @return + * New environment variables dictionary. + */ +export function createEnvValues( + source: NodeJS.ProcessEnv, + valuesToMerge: Record +): NodeJS.ProcessEnv { + const findTarget = (obj: any, key: string) => { + if (platform() === 'win32') { + return ( + Object.keys(obj).find( + (i) => + i.localeCompare(key, undefined, { + sensitivity: 'accent', + }) === 0 + ) || key + ); + } + return key; + }; + const result = { ...source }; + for (const [key, value] of Object.entries(valuesToMerge)) { + const target = findTarget(result, key); + if (value === null) { + delete result[target]; + } else { + result[target] = value; + } + } + return result; +} + +/** + * Calculate the CWD that should be used to launch gdb based on the program + * being debugged or the explicitly set cwd in the launch arguments. + * + * Note that launchArgs.program is optional here in preparation for + * debugging where no program is specified. See #262 + * + * @param launchArgs Launch Arguments to compute GDB cwd from + * @returns effective cwd to use + */ +export function getGdbCwd(launchArgs: { + program?: string; + cwd?: string; +}): string { + const cwd = + launchArgs.cwd || + (launchArgs.program && existsSync(launchArgs.program) + ? dirname(launchArgs.program) + : process.cwd()); + return existsSync(cwd) ? cwd : process.cwd(); +} diff --git a/src/cdtDebugAdapter/adapter/varManager.ts b/src/cdtDebugAdapter/adapter/varManager.ts new file mode 100644 index 000000000..d6b75c24f --- /dev/null +++ b/src/cdtDebugAdapter/adapter/varManager.ts @@ -0,0 +1,178 @@ +import { GDBBackend } from './GDBBackend'; +import { MIVarCreateResponse } from './mi/var'; +import { sendVarCreate, sendVarDelete, sendVarUpdate } from './mi/var'; + +export interface VarObjType { + varname: string; + expression: string; + numchild: string; + children: VarObjType[]; + value: string; + type: string; + isVar: boolean; + isChild: boolean; + varType: string; +} + +export class VarManager { + protected readonly variableMap: Map = new Map< + string, + VarObjType[] + >(); + + constructor(protected gdb: GDBBackend) { + this.gdb = gdb; + } + + public getKey(frameId: number, threadId: number, depth: number): string { + return `frame${frameId}_thread${threadId}_depth${depth}`; + } + + public getVars( + frameId: number, + threadId: number, + depth: number + ): VarObjType[] | undefined { + return this.variableMap.get(this.getKey(frameId, threadId, depth)); + } + + public getVar( + frameId: number, + threadId: number, + depth: number, + expression: string, + type?: string + ): VarObjType | undefined { + const vars = this.getVars(frameId, threadId, depth); + if (vars) { + for (const varobj of vars) { + if (varobj.expression === expression) { + if (type !== 'registers') { + type = 'local'; + } + if (type === varobj.varType) { + return varobj; + } + } + } + } + return; + } + + public getVarByName( + frameId: number, + threadId: number, + depth: number, + varname: string + ): VarObjType | undefined { + const vars = this.getVars(frameId, threadId, depth); + if (vars) { + for (const varobj of vars) { + if (varobj.varname === varname) { + return varobj; + } + } + } + return; + } + + public addVar( + frameId: number, + threadId: number, + depth: number, + expression: string, + isVar: boolean, + isChild: boolean, + varCreateResponse: MIVarCreateResponse, + type?: string + ): VarObjType { + let vars = this.variableMap.get(this.getKey(frameId, threadId, depth)); + if (!vars) { + vars = []; + this.variableMap.set(this.getKey(frameId, threadId, depth), vars); + } + const varobj: VarObjType = { + varname: varCreateResponse.name, + expression, + numchild: varCreateResponse.numchild, + children: [], + value: varCreateResponse.value, + type: varCreateResponse.type, + isVar, + isChild, + varType: type ? type : 'local', + }; + vars.push(varobj); + return varobj; + } + + public async removeVar( + frameId: number, + threadId: number, + depth: number, + varname: string + ): Promise { + let deleteme: VarObjType | undefined; + const vars = this.variableMap.get( + this.getKey(frameId, threadId, depth) + ); + if (vars) { + for (const varobj of vars) { + if (varobj.varname === varname) { + deleteme = varobj; + break; + } + } + if (deleteme) { + await sendVarDelete(this.gdb, { varname: deleteme.varname }); + vars.splice(vars.indexOf(deleteme), 1); + for (const child of deleteme.children) { + await this.removeVar( + frameId, + threadId, + depth, + child.varname + ); + } + } + } + } + + public async updateVar( + frameId: number, + threadId: number, + depth: number, + varobj: VarObjType + ): Promise { + let returnVar = varobj; + const vup = await sendVarUpdate(this.gdb, { name: varobj.varname }); + const update = vup.changelist[0]; + if (update) { + if (update.in_scope === 'true') { + if (update.name === varobj.varname) { + // don't update the parent value to a child's value + varobj.value = update.value; + } + } else { + this.removeVar(frameId, threadId, depth, varobj.varname); + await sendVarDelete(this.gdb, { varname: varobj.varname }); + const createResponse = await sendVarCreate(this.gdb, { + frame: 'current', + expression: varobj.expression, + frameId: frameId, + threadId: threadId, + }); + returnVar = this.addVar( + frameId, + threadId, + depth, + varobj.expression, + varobj.isVar, + varobj.isChild, + createResponse + ); + } + } + return Promise.resolve(returnVar); + } +} diff --git a/src/cdtDebugAdapter/debugConfProvider.ts b/src/cdtDebugAdapter/debugConfProvider.ts new file mode 100644 index 000000000..092e92f0d --- /dev/null +++ b/src/cdtDebugAdapter/debugConfProvider.ts @@ -0,0 +1,140 @@ +/* + * Project: ESP-IDF VSCode Extension + * File Created: Monday, 26th February 2024 2:54:26 pm + * Copyright 2024 Espressif Systems (Shanghai) CO LTD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + CancellationToken, + DebugConfiguration, + DebugConfigurationProvider, + WorkspaceFolder, +} from "vscode"; +import { readParameter } from "../idfConfiguration"; +import { getProjectName } from "../workspaceConfig"; +import { join } from "path"; +import { pathExists } from "fs-extra"; +import { verifyAppBinary } from "../espIdf/debugAdapter/verifyApp"; +import { OpenOCDManager } from "../espIdf/openOcd/openOcdManager"; +import { Logger } from "../logger/logger"; +import { getToolchainPath } from "../utils"; + +export class CDTDebugConfigurationProvider + implements DebugConfigurationProvider { + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + config: DebugConfiguration, + token?: CancellationToken + ): Promise { + try { + if (!config.program) { + const buildDirPath = readParameter("idf.buildPath", folder) as string; + const projectName = await getProjectName(buildDirPath); + const elfFilePath = join(buildDirPath, `${projectName}.elf`); + const elfFileExists = await pathExists(elfFilePath); + if (!elfFileExists) { + throw new Error( + `${elfFilePath} doesn't exist. Build this project first.` + ); + } + config.program = elfFilePath; + } + if (!config.gdb) { + config.gdb = await getToolchainPath(folder.uri, "gdb"); + } + if ( + config.sessionID !== "core-dump.debug.session.ws" && + config.sessionID !== "gdbstub.debug.session.ws" && + (!config.initCommands || config.initCommands.length === 0) + ) { + config.initCommands = [ + "set remote hardware-watchpoint-limit {IDF_TARGET_CPU_WATCHPOINT_NUM}", + "mon reset halt", + "maintenance flush register-cache", + "thb app_main", + ]; + } + + if (config.initCommands && Array.isArray(config.initCommands)) { + let idfTarget = readParameter("idf.adapterTargetName", folder); + if (idfTarget === "custom") { + idfTarget = readParameter("idf.customAdapterTargetName", folder); + } + type IdfTarget = + | "esp32" + | "esp32s2" + | "esp32s3" + | "esp32c2" + | "esp32c3" + | "esp32c6" + | "esp32h2"; + // Mapping of idfTarget to corresponding CPU watchpoint numbers + const idfTargetWatchpointMap: Record = { + esp32: 2, + esp32s2: 2, + esp32s3: 2, + esp32c2: 2, + esp32c3: 8, + esp32c6: 4, + esp32h2: 4, + }; + config.initCommands = config.initCommands.map((cmd: string) => + cmd.replace( + "{IDF_TARGET_CPU_WATCHPOINT_NUM}", + idfTargetWatchpointMap[idfTarget] + ) + ); + } + + if ( + config.sessionID !== "core-dump.debug.session.ws" && + config.sessionID !== "gdbstub.debug.session.ws" && + !config.target + ) { + config.target = { + connectCommands: [ + "set remotetimeout 20", + "-target-select extended-remote localhost:3333", + ], + }; + } + if (folder && folder.uri && config.verifyAppBinBeforeDebug) { + const isSameAppBinary = await verifyAppBinary(folder.uri); + if (!isSameAppBinary) { + throw new Error( + `Current app binary is different from your project. Flash first.` + ); + } + } + const openOCDManager = OpenOCDManager.init(); + if ( + !openOCDManager.isRunning() && + config.sessionID !== "core-dump.debug.session.ws" && + config.sessionID !== "gdbstub.debug.session.ws" && + config.sessionID !== "qemu.debug.session" && + !config.runOpenOCD + ) { + await openOCDManager.start(); + } + } catch (error) { + const msg = error.message + ? error.message + : "Some build files doesn't exist. Build this project first."; + Logger.error(msg, error); + return; + } + return config; + } +} diff --git a/src/cdtDebugAdapter/server.ts b/src/cdtDebugAdapter/server.ts new file mode 100644 index 000000000..0de92a90f --- /dev/null +++ b/src/cdtDebugAdapter/server.ts @@ -0,0 +1,58 @@ +/* + * Project: ESP-IDF VSCode Extension + * File Created: Monday, 4th March 2024 12:12:14 pm + * Copyright 2024 Espressif Systems (Shanghai) CO LTD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AddressInfo, Server, createServer } from "net"; +import { + DebugAdapterDescriptor, + DebugAdapterDescriptorFactory, + DebugAdapterExecutable, + DebugAdapterServer, + DebugSession, + ProviderResult, +} from "vscode"; +import { GDBTargetDebugSession } from "./adapter"; + +const DEBUG_DEFAULT_PORT = 43476; + +export class CDTDebugAdapterDescriptorFactory + implements DebugAdapterDescriptorFactory { + private server?: Server; + createDebugAdapterDescriptor( + session: DebugSession, + executable: DebugAdapterExecutable | undefined + ): ProviderResult { + if (!this.server) { + // start listening on a random port + const portToUse = session.configuration.debugPort || DEBUG_DEFAULT_PORT; + this.server = createServer((socket) => { + const gdbTargetDebugSession = new GDBTargetDebugSession(); + gdbTargetDebugSession.setRunAsServer(true); + gdbTargetDebugSession.start(socket, socket); + }).listen(portToUse); + } + + // make VS Code connect to debug server + return new DebugAdapterServer((this.server.address()).port); + } + + dispose() { + if (this.server) { + this.server.close(); + } + } +} diff --git a/src/espIdf/debugAdapter/checkPyReqs.ts b/src/espIdf/debugAdapter/checkPyReqs.ts new file mode 100644 index 000000000..b3f92724f --- /dev/null +++ b/src/espIdf/debugAdapter/checkPyReqs.ts @@ -0,0 +1,51 @@ +/* + * Project: ESP-IDF VSCode Extension + * File Created: Friday, 23rd February 2024 6:13:58 pm + * Copyright 2024 Espressif Systems (Shanghai) CO LTD + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { join } from "path"; +import { readParameter } from "../../idfConfiguration"; +import { pathExists } from "fs-extra"; +import { Uri } from "vscode"; +import { extensionContext, startPythonReqsProcess } from "../../utils"; + +export async function checkDebugAdapterRequirements(workspaceFolder: Uri) { + const idfPath = readParameter("idf.espIdfPath", workspaceFolder); + const pythonBinPath = readParameter("idf.pythonBinPath", workspaceFolder); + let requirementsPath = join( + extensionContext.extensionPath, + "esp_debug_adapter", + "requirements.txt" + ); + let checkResult: string; + try { + const doesPyTestRequirementsExists = await pathExists(requirementsPath); + if (!doesPyTestRequirementsExists) { + return false; + } + checkResult = await startPythonReqsProcess( + pythonBinPath, + idfPath, + requirementsPath + ); + } catch (error) { + checkResult = error && error.message ? error.message : " are not satisfied"; + } + if (checkResult.indexOf("are satisfied") > -1) { + return true; + } + return false; +} diff --git a/src/espIdf/debugAdapter/nodes/peripheral.ts b/src/espIdf/debugAdapter/nodes/peripheral.ts index d0ccd337c..1a3ef4154 100644 --- a/src/espIdf/debugAdapter/nodes/peripheral.ts +++ b/src/espIdf/debugAdapter/nodes/peripheral.ts @@ -152,47 +152,41 @@ export class Peripheral extends PeripheralBaseNode { return this.format; } - public updateData(): Thenable { - return new Promise((resolve, reject) => { - if (!this.expanded) { - return resolve(false); - } + public async updateData(): Promise { + if (!this.expanded) { + return false; + } - this.readMemory() - .then((unused) => { - this.updateChildData(resolve, reject, null); - }) - .catch((error) => { - const msg = error.message || "unknown error"; - const str = `Failed to update peripheral ${this.name}: ${msg}`; - if (debug.activeDebugConsole) { + try { + const errors = await this.readMemory(); + for (const error of errors) { + const str = `Failed to update peripheral ${this.name}: ${error}`; + if (debug.activeDebugConsole) { + debug.activeDebugConsole.appendLine(str); + } + } + } catch (e) { + const msg = (e as Error).message || 'unknown error'; + const str = `Failed to update peripheral ${this.name}: ${msg}`; + if (debug.activeDebugConsole) { debug.activeDebugConsole.appendLine(str); - } - this.updateChildData(null, reject, new Error(str)); - }); - }); - } - - private updateChildData(resolve, reject, err: Error) { - if (err) { - return reject(err); + } } - const promises = this.children.map((r) => r.updateData()); - Promise.all(promises) - .then((result) => { - return resolve(true); - }) - .catch((err) => { - const msg = err.message || "unknown error"; - const str = `Failed to update peripheral ${this.name}: ${msg}`; + + try { + const promises = this.children.map((r) => r.updateData()); + await Promise.all(promises); + return true; + } catch (e) { + const str = `Internal error: Failed to update peripheral ${this.name} after memory reads`; if (debug.activeDebugConsole) { - debug.activeDebugConsole.appendLine(str); + debug.activeDebugConsole.appendLine(str); } - return reject(err ? err : new Error(str)); - }); - } + return true; + } +} - private readMemory(): Promise { + private readMemory(): Promise { if (!this.currentValue) { this.currentValue = new Array(this.totalLength); } diff --git a/src/espIdf/debugAdapter/nodes/register.ts b/src/espIdf/debugAdapter/nodes/register.ts index a58ea5f74..e08e76d63 100644 --- a/src/espIdf/debugAdapter/nodes/register.ts +++ b/src/espIdf/debugAdapter/nodes/register.ts @@ -333,21 +333,25 @@ export class Register extends PeripheralBaseNode { const bc = this.size / 8; const bytes = this.parent.getBytes(this.offset, bc); const buffer = Buffer.from(bytes); - switch (bc) { - case 1: - this.currentValue = buffer.readUInt8(0); - break; - case 2: - this.currentValue = buffer.readUInt16LE(0); - break; - case 4: - this.currentValue = buffer.readUInt32LE(0); - break; - default: - window.showErrorMessage( - `Register ${this.name} has invalid size: ${this.size}. Should be 8, 16 or 32.` - ); - break; + try { + switch (bc) { + case 1: + this.currentValue = buffer.readUInt8(0); + break; + case 2: + this.currentValue = buffer.readUInt16LE(0); + break; + case 4: + this.currentValue = buffer.readUInt32LE(0); + break; + default: + window.showErrorMessage( + `Register ${this.name} has invalid size: ${this.size}. Should be 8, 16 or 32.` + ); + break; + } + } catch (error) { + return Promise.reject(error); } this.children.forEach((f) => f.updateData()); diff --git a/src/espIdf/debugAdapter/peripheralTreeView.ts b/src/espIdf/debugAdapter/peripheralTreeView.ts index 3e3c0604b..a3c347f00 100644 --- a/src/espIdf/debugAdapter/peripheralTreeView.ts +++ b/src/espIdf/debugAdapter/peripheralTreeView.ts @@ -326,7 +326,7 @@ export class PeripheralTreeView public debugContinued() {} public getActiveDebugSession() { - return debug.activeDebugSession?.type === "espidf" + return debug.activeDebugSession?.type === "gdbtarget" ? debug.activeDebugSession : null; } diff --git a/src/espIdf/debugAdapter/utils.ts b/src/espIdf/debugAdapter/utils.ts index 81af184ab..53c8aa94d 100644 --- a/src/espIdf/debugAdapter/utils.ts +++ b/src/espIdf/debugAdapter/utils.ts @@ -134,56 +134,38 @@ export function parseInteger(value: string): number { return undefined; } -export function readMemoryChunks( +export async function readMemoryChunks( session: DebugSession, startAddr: number, specs: AddrRange[], storeTo: number[] -): Promise { - const promises = specs.map((r) => { - return new Promise((resolve, reject) => { - const addr = "0x" + r.base.toString(16); - session - .customRequest("readMemory", { memoryReference: addr, count: r.length, offset: 0 }) - .then( - (result) => { - let dst = r.base - startAddr; - const bytes = []; - const numBytes = result.data[0].contents.length / 2; - const data = result.data[0].contents; - for (let i = 0, d = 0; i < numBytes; i++, d +=2) { - bytes.push([`${data[d]}${data[d+1]}`]); - } - for (const byte of bytes) { - storeTo[dst++] = byte; - } - resolve(true); - }, - (e) => { - let dst = r.base - startAddr; - for (let ix = 0; ix < r.length; ix++) { - storeTo[dst++] = 0xff; - } - reject(e); - } - ); - }); - }); - - return new Promise(async (resolve, reject) => { - const results = await Promise.all(promises.map((p) => p.catch((e) => e))); - const errs: string[] = []; - results.map((e) => { - if (e instanceof Error) { - errs.push(e.message); +): Promise { + const errors: Error[] = []; + for (const spec of specs) { + const memoryReference = "0x" + spec.base.toString(16); + + try { + const responseBody = await session.customRequest("readMemory", { + memoryReference, + count: spec.length, + }); + if (responseBody && responseBody.data) { + const bytes = Buffer.from(responseBody.data, "base64"); + let dst = spec.base - startAddr; + for (const byte of bytes) { + storeTo[dst++] = byte; + } } - }); - if (errs.length !== 0) { - reject(new Error(errs.join("\n"))); - } else { - resolve(true); + } catch (e) { + const err = e ? e.toString() : "Unknown error"; + errors.push( + new Error( + `peripheral-viewer: readMemory failed @ ${memoryReference} for ${spec.length} bytes: ${err}, session=${session.id}` + ) + ); } - }); + } + return errors; } export function readMemory( @@ -191,7 +173,7 @@ export function readMemory( startAddr: number, length: number, storeTo: number[] -): Promise { +): Promise { const maxChunk = 4 * 1024; const ranges = splitIntoChunks([new AddrRange(startAddr, length)], maxChunk); return readMemoryChunks(session, startAddr, ranges, storeTo); diff --git a/src/espIdf/debugAdapter/verifyApp.ts b/src/espIdf/debugAdapter/verifyApp.ts index 4dcb2086e..c51d75dd7 100644 --- a/src/espIdf/debugAdapter/verifyApp.ts +++ b/src/espIdf/debugAdapter/verifyApp.ts @@ -2,13 +2,13 @@ * Project: ESP-IDF VSCode Extension * File Created: Friday, 16th July 2021 4:23:24 pm * Copyright 2021 Espressif Systems (Shanghai) CO LTD - *  + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *  + * * http://www.apache.org/licenses/LICENSE-2.0 - *  + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import { createFlashModel } from "../../flash/flashModelBuilder"; import { readParameter } from "../../idfConfiguration"; import { Logger } from "../../logger/logger"; import { appendIdfAndToolsToPath, spawn } from "../../utils"; +import { pathExists } from "fs-extra"; export async function verifyAppBinary(workspaceFolder: Uri) { const modifiedEnv = appendIdfAndToolsToPath(workspaceFolder); @@ -44,6 +45,12 @@ export async function verifyAppBinary(workspaceFolder: Uri) { workspaceFolder ) as string; const flasherArgsJsonPath = join(buildDirPath, "flasher_args.json"); + const flasherArgsJsonPathExists = await pathExists(flasherArgsJsonPath); + if (!flasherArgsJsonPathExists) { + return Logger.info( + `${flasherArgsJsonPath} doesn't exist. Build the project first` + ); + } const model = await createFlashModel( flasherArgsJsonPath, serialPort, @@ -62,7 +69,7 @@ export async function verifyAppBinary(workspaceFolder: Uri) { `build/${model.app.binFilePath}`, ], { - cwd: workspaceFolder, + cwd: workspaceFolder.fsPath, env: modifiedEnv, } ); @@ -80,15 +87,12 @@ export async function verifyAppBinary(workspaceFolder: Uri) { } catch (error) { if ( error && - error.error && - error.error.message && - error.error.message.indexOf("verify FAILED (digest mismatch)") !== -1 + error.message && + error.message.indexOf("verify FAILED (digest mismatch)") !== -1 ) { return false; } - const msg = error.error.message - ? error.error.message - : error.message + const msg = error.message ? error.message : "Something wrong while verifying app binary."; Logger.errorNotify(msg, error); diff --git a/src/espIdf/monitor/index.ts b/src/espIdf/monitor/index.ts index 9f49933bc..1222672f3 100644 --- a/src/espIdf/monitor/index.ts +++ b/src/espIdf/monitor/index.ts @@ -16,6 +16,7 @@ * limitations under the License. */ +import { ESP } from "../../config"; import { appendIdfAndToolsToPath } from "../../utils"; import { window, Terminal, Uri, env } from "vscode"; @@ -103,7 +104,8 @@ export class IDFMonitor { } async dispose() { try { - process.kill(await this.terminal.processId); + this.terminal.sendText(ESP.CTRL_RBRACKET); + this.terminal.sendText(`exit`); } catch (error) {} } } diff --git a/src/extension.ts b/src/extension.ts index 34ca347b1..6438abd71 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -145,6 +145,9 @@ import { saveDefSdkconfig } from "./espIdf/menuconfig/saveDefConfig"; import { createSBOM, installEspSBOM } from "./espBom"; import { getEspHomeKitSdk } from "./espHomekit/espHomekitDownload"; import { getCurrentIdfSetup, selectIdfSetup } from "./versionSwitcher"; +import { checkDebugAdapterRequirements } from "./espIdf/debugAdapter/checkPyReqs"; +import { CDTDebugConfigurationProvider } from "./cdtDebugAdapter/debugConfProvider"; +import { CDTDebugAdapterDescriptorFactory } from "./cdtDebugAdapter/server"; // Global variables shared by commands let workspaceRoot: vscode.Uri; @@ -156,6 +159,7 @@ let statusBarItems: { [key: string]: vscode.StatusBarItem }; const openOCDManager = OpenOCDManager.init(); let isOpenOCDLaunchedByDebug: boolean = false; +let isDebugRestarted: boolean = false; let debugAdapterManager: DebugAdapterManager; let isMonitorLaunchedByDebug: boolean = false; @@ -412,7 +416,7 @@ export async function activate(context: vscode.ExtensionContext) { }); vscode.debug.onDidTerminateDebugSession((e) => { - if (isOpenOCDLaunchedByDebug) { + if (isOpenOCDLaunchedByDebug && !isDebugRestarted) { isOpenOCDLaunchedByDebug = false; openOCDManager.stop(); } @@ -1321,6 +1325,19 @@ export async function activate(context: vscode.ExtensionContext) { vscode.debug.registerDebugConfigurationProvider("espidf", debugProvider) ); + const cdtDebugProvider = new CDTDebugConfigurationProvider(); + context.subscriptions.push( + vscode.debug.registerDebugConfigurationProvider( + "gdbtarget", + cdtDebugProvider + ) + ); + + vscode.debug.registerDebugAdapterDescriptorFactory( + "gdbtarget", + new CDTDebugAdapterDescriptorFactory() + ); + vscode.debug.registerDebugAdapterDescriptorFactory("espidf", { async createDebugAdapterDescriptor(session: vscode.DebugSession) { try { @@ -1331,8 +1348,7 @@ export async function activate(context: vscode.ExtensionContext) { workspaceRoot ); if ( - (session.configuration.sessionID !== "core-dump.debug.session.ws" || - session.configuration.sessionID !== "gdbstub.debug.session.ws") && + session.configuration.sessionID !== "core-dump.debug.session.ws" && useMonitorWithDebug ) { isMonitorLaunchedByDebug = true; @@ -1341,8 +1357,7 @@ export async function activate(context: vscode.ExtensionContext) { if ( launchMode === "auto" && !openOCDManager.isRunning() && - session.configuration.sessionID !== "core-dump.debug.session.ws" && - session.configuration.sessionID !== "qemu.debug.session" + session.configuration.sessionID !== "core-dump.debug.session.ws" ) { isOpenOCDLaunchedByDebug = true; await openOCDManager.start(); @@ -1357,10 +1372,7 @@ export async function activate(context: vscode.ExtensionContext) { debugAdapterManager.configureAdapter(debugAdapterConfig); await debugAdapterManager.start(); } - if ( - session.configuration.sessionID === "core-dump.debug.session.ws" || - session.configuration.sessionID === "gdbstub.debug.session.ws" - ) { + if (session.configuration.sessionID === "core-dump.debug.session.ws") { await debugAdapterManager.start(); } if (launchMode === "auto" && !debugAdapterManager.isRunning()) { @@ -1390,12 +1402,48 @@ export async function activate(context: vscode.ExtensionContext) { }, }); - vscode.debug.onDidStartDebugSession((session) => { + vscode.debug.onDidStartDebugSession(async (session) => { const svdFile = idfConf.readParameter( "idf.svdFilePath", workspaceRoot ) as string; peripheralTreeProvider.debugSessionStarted(session, svdFile, 16); // Move svdFile and threshold as conf settings + if ( + openOCDManager.isRunning() && + session.type === "gdbtarget" && + session.configuration.sessionID !== "core-dump.debug.session.ws" && + session.configuration.sessionID !== "gdbstub.debug.session.ws" + ) { + isOpenOCDLaunchedByDebug = true; + } + isDebugRestarted = false; + }); + + vscode.debug.registerDebugAdapterTrackerFactory("gdbtarget", { + createDebugAdapterTracker(session: vscode.DebugSession) { + return { + onDidSendMessage: async (m) => { + if (m && m.type === "event" && m.event === "stopped") { + const peripherals = await peripheralTreeProvider.getChildren(); + for (const p of peripherals) { + p.getPeripheral().updateData(); + } + peripheralTreeProvider.refresh(); + } + + if ( + m && + m.type === "event" && + m.event === "output" && + m.body.output.indexOf( + `From client: disconnect({"restart":true})` + ) !== -1 + ) { + isDebugRestarted = true; + } + }, + }; + }, }); vscode.debug.onDidTerminateDebugSession((session) => { @@ -1740,39 +1788,15 @@ export async function activate(context: vscode.ExtensionContext) { }); }); - registerIDFCommand("espIdf.getXtensaGdb", () => { + registerIDFCommand("espIdf.getToolchainGdb", () => { return PreCheck.perform([openFolderCheck], async () => { - const modifiedEnv = utils.appendIdfAndToolsToPath(workspaceRoot); - const idfTarget = modifiedEnv.IDF_TARGET || "esp32"; - const gdbTool = utils.getToolchainToolName(idfTarget, "gdb"); - try { - return await utils.isBinInPath( - gdbTool, - workspaceRoot.fsPath, - modifiedEnv - ); - } catch (error) { - Logger.errorNotify("gdb is not found in idf.customExtraPaths", error); - return; - } + return await utils.getToolchainPath(workspaceRoot, "gdb"); }); }); - registerIDFCommand("espIdf.getXtensaGcc", () => { + registerIDFCommand("espIdf.getToolchainGcc", () => { return PreCheck.perform([openFolderCheck], async () => { - const modifiedEnv = utils.appendIdfAndToolsToPath(workspaceRoot); - const idfTarget = modifiedEnv.IDF_TARGET || "esp32"; - const gccTool = utils.getToolchainToolName(idfTarget, "gcc"); - try { - return await utils.isBinInPath( - gccTool, - workspaceRoot.fsPath, - modifiedEnv - ); - } catch (error) { - Logger.errorNotify("gcc is not found in idf.customExtraPaths", error); - return; - } + return await utils.getToolchainPath(workspaceRoot, "gcc"); }); }); @@ -2490,8 +2514,9 @@ export async function activate(context: vscode.ExtensionContext) { registerIDFCommand("espIdf.qemuDebug", () => { PreCheck.perform([openFolderCheck], async () => { - if (qemuManager.isRunning()) { - qemuManager.stop(); + if (monitorTerminal) { + monitorTerminal.sendText(ESP.CTRL_RBRACKET); + monitorTerminal.sendText(`exit`); } const buildDirPath = idfConf.readParameter( "idf.buildPath", @@ -2503,35 +2528,33 @@ export async function activate(context: vscode.ExtensionContext) { if (!qemuBinExists) { await mergeFlashBinaries(workspaceRoot); } - qemuManager.configure({ - launchArgs: [ - "-nographic", - "-gdb tcp::3333", - "-S", - "-machine", - "esp32", - "-drive", - "file=build/merged_qemu.bin,if=mtd,format=raw", - ], - } as IQemuOptions); - await qemuManager.start(); - const debugAdapterConfig = { - initGdbCommands: [ - "target remote localhost:3333", - "monitor system_reset", - "tb app_main", - "c", - ], - isPostMortemDebugMode: false, - isOocdDisabled: true, - logLevel: 5, - } as IDebugAdapterConfig; - debugAdapterManager.configureAdapter(debugAdapterConfig); - await vscode.debug.startDebugging(undefined, { + if (qemuManager.isRunning()) { + qemuManager.stop(); + await utils.sleep(1000); + } + qemuManager.configureWithDefValues(); + qemuManager.start(); + const gdbPath = await utils.getToolchainPath(workspaceRoot, "gdb"); + const workspaceFolder = vscode.workspace.getWorkspaceFolder( + workspaceRoot + ); + await vscode.debug.startDebugging(workspaceFolder, { name: "GDB QEMU", - type: "espidf", - request: "launch", + type: "gdbtarget", + request: "attach", sessionID: "qemu.debug.session", + gdb: gdbPath, + initCommands: [ + "set remote hardware-watchpoint-limit {IDF_TARGET_CPU_WATCHPOINT_NUM}", + "mon reset halt", + "maintenance flush register-cache", + "thb app_main", + ], + target: { + type: "remote", + host: "localhost", + port: "1234", + }, }); vscode.debug.onDidTerminateDebugSession(async (session) => { if (session.configuration.sessionID === "qemu.debug.session") { @@ -3047,6 +3070,7 @@ export async function activate(context: vscode.ExtensionContext) { } const toolchainPrefix = utils.getToolchainToolName(idfTarget, ""); const projectName = await getProjectName(buildDirPath); + const gdbPath = await utils.getToolchainPath(workspaceRoot, "gdb"); const elfFilePath = path.join(buildDirPath, `${projectName}.elf`); const wsPort = idfConf.readParameter("idf.wssPort", workspaceRoot); const idfVersion = await utils.getEspIdfFromCMake(idfPath); @@ -3144,23 +3168,30 @@ export async function activate(context: vscode.ExtensionContext) { ), }); try { - debugAdapterManager.configureAdapter({ - isPostMortemDebugMode: true, - elfFile: resp.prog, - coreDumpFile: coreElfFilePath, - isOocdDisabled: true, - }); - await vscode.debug.startDebugging(undefined, { + const workspaceFolder = vscode.workspace.getWorkspaceFolder( + workspaceRoot + ); + await vscode.debug.startDebugging(workspaceFolder, { name: "Core Dump Debug", - type: "espidf", - request: "launch", sessionID: "core-dump.debug.session.ws", + type: "gdbtarget", + request: "attach", + gdb: gdbPath, + program: resp.prog, + logFile: `${path.join( + workspaceRoot.fsPath, + "coredump.log" + )}`, + target: { + connectCommands: [`core ${coreElfFilePath}`], + }, }); vscode.debug.onDidTerminateDebugSession((session) => { if ( session.configuration.sessionID === "core-dump.debug.session.ws" ) { + wsServer.done(); monitor.dispose(); wsServer.close(); } @@ -3182,26 +3213,27 @@ export async function activate(context: vscode.ExtensionContext) { ); }) .on("gdb-stub-detected", async (resp) => { - const setupCmd = [`target remote ${resp.port}`]; - const debugAdapterConfig = { - elfFile: resp.prog, - initGdbCommands: setupCmd, - isOocdDisabled: false, - isPostMortemDebugMode: true, - logLevel: 5, - } as IDebugAdapterConfig; try { - debugAdapterManager.configureAdapter(debugAdapterConfig); - await vscode.debug.startDebugging(undefined, { + const workspaceFolder = vscode.workspace.getWorkspaceFolder( + workspaceRoot + ); + await vscode.debug.startDebugging(workspaceFolder, { name: "GDB Stub Debug", - type: "espidf", - request: "launch", + type: "gdbtarget", + request: "attach", sessionID: "gdbstub.debug.session.ws", + gdb: gdbPath, + program: resp.prog, + logFile: `${path.join(workspaceRoot.fsPath, "gdbstub.log")}`, + target: { + connectCommands: [`target remote ${resp.port}`], + }, }); vscode.debug.onDidTerminateDebugSession((session) => { if ( session.configuration.sessionID === "gdbstub.debug.session.ws" ) { + wsServer.done(); monitor.dispose(); wsServer.close(); } @@ -3912,17 +3944,26 @@ function createQemuMonitor( customTimestampFormat: string = "" ) { PreCheck.perform([openFolderCheck], async () => { - const isQemuLaunched = await qemuManager.isRunning(); - if (!isQemuLaunched) { - vscode.window.showInformationMessage( - vscode.l10n.t("QEMU is not running. Run first.") - ); - return; + const isQemuLaunched = qemuManager.isRunning(); + if (isQemuLaunched) { + qemuManager.stop(); } const qemuTcpPort = idfConf.readParameter( "idf.qemuTcpPort", workspaceRoot ) as number; + qemuManager.configure({ + launchArgs: [ + "-nographic", + "-machine", + "esp32", + "-drive", + "file=build/merged_qemu.bin,if=mtd,format=raw", + "-monitor stdio", + `-serial tcp::${qemuTcpPort},server,nowait`, + ], + } as IQemuOptions); + qemuManager.start(); const serialPort = `socket://localhost:${qemuTcpPort}`; const idfMonitor = await createNewIdfMonitor( workspaceRoot, @@ -4183,6 +4224,19 @@ class IdfDebugConfigurationProvider } } config.elfFilePath = elfFilePath; + const debugAdapterPackagesExist = await checkDebugAdapterRequirements( + workspaceRoot + ); + if (!debugAdapterPackagesExist) { + const installDAPyPkgs = await vscode.window.showInformationMessage( + "ESP-IDF Debug Adapter Python packages are not installed", + "Install" + ); + if (installDAPyPkgs && installDAPyPkgs === "Install") { + await vscode.commands.executeCommand("espIdf.installPyReqs"); + } + return; + } } catch (error) { const msg = error.message ? error.message diff --git a/src/pythonManager.ts b/src/pythonManager.ts index 79e49eb13..f56280a07 100644 --- a/src/pythonManager.ts +++ b/src/pythonManager.ts @@ -114,15 +114,6 @@ export async function installPythonEnvFromIdfTools( ? ["Scripts", "python.exe"] : ["bin", "python"]; const virtualEnvPython = join(pyEnvPath, ...pyDir); - await installExtensionPyReqs( - virtualEnvPython, - espDir, - idfToolsDir, - pyTracker, - channel, - { env: modifiedEnv }, - cancelToken - ); return virtualEnvPython; } diff --git a/src/qemu/qemuManager.ts b/src/qemu/qemuManager.ts index b4340da7d..e531cfece 100644 --- a/src/qemu/qemuManager.ts +++ b/src/qemu/qemuManager.ts @@ -123,14 +123,15 @@ export class QemuManager extends EventEmitter { const defOptions = { launchArgs: [ "-nographic", + "-s", + "-S", "-machine", "esp32", "-drive", "file=build/merged_qemu.bin,if=mtd,format=raw", - "-monitor stdio" ], tcpPort: readParameter("idf.qemuTcpPort", workspaceFolder), - workspaceFolder + workspaceFolder, } as IQemuOptions; this.configure(defOptions); } @@ -186,9 +187,6 @@ export class QemuManager extends EventEmitter { this.options.launchArgs.forEach((arg) => { qemuArgs.push(arg); }); - qemuArgs.push( - `-serial tcp::${this.options.tcpPort.toString()},server,nowait` - ); if (typeof this.qemuTerminal === "undefined") { this.qemuTerminal = window.createTerminal({ @@ -203,7 +201,7 @@ export class QemuManager extends EventEmitter { strictEnv: true, }); window.onDidCloseTerminal((e) => { - if (e.name === "ESP-IDF QEMU") { + if (e.name === this.qemuTerminal.name) { this.stop(); } }); diff --git a/src/support/writeReport.ts b/src/support/writeReport.ts index bed65c8f1..22bcda505 100644 --- a/src/support/writeReport.ts +++ b/src/support/writeReport.ts @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { writeFile, writeJson } from "fs-extra"; +import { pathExists, readFile, writeFile, writeJson } from "fs-extra"; import { EOL } from "os"; import { join } from "path"; import * as vscode from "vscode"; @@ -195,18 +195,24 @@ export async function writeTextReport( } if (reportedResult.latestError) { output += `----------------------------------------------------------- Latest error -----------------------------------------------------------------${EOL}`; - output += `Latest error at ${ - reportedResult.latestError.message - ? reportedResult.latestError.message - : typeof reportedResult.latestError === "string" - ? reportedResult.latestError - : "Unknown error in ESP-IDF Doctor Command" - }${EOL}`; + output += + JSON.stringify( + reportedResult.latestError, + undefined, + vscode.workspace.getConfiguration().get("editor.tabSize") || 2 + ) + EOL; } output += lineBreak; + const logFile = join(context.extensionPath, "esp_idf_vsc_ext.log"); + const logFileExists = await pathExists(logFile); + if (logFileExists) { + const logFileContent = await readFile(logFile, "utf8"); + output += `----------------------------------------------------------- Logfile -----------------------------------------------------------------${EOL}`; + output += logFileContent + EOL + lineBreak; + } const resultFile = join(context.extensionPath, "report.txt"); - const resultJson = join(context.extensionPath, "report.json"); await writeFile(resultFile, output); + const resultJson = join(context.extensionPath, "report.json"); await writeJson(resultJson, reportedResult, { spaces: vscode.workspace.getConfiguration().get("editor.tabSize") || 2, }); diff --git a/src/utils.ts b/src/utils.ts index 15c2cb6ad..dc8b1e193 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -253,6 +253,21 @@ export async function setCCppPropertiesJsonCompilerPath( } } +export async function getToolchainPath( + workspaceUri: vscode.Uri, + tool: string = "gcc" +) { + const modifiedEnv = appendIdfAndToolsToPath(workspaceUri); + const idfTarget = modifiedEnv.IDF_TARGET || "esp32"; + const gccTool = getToolchainToolName(idfTarget, tool); + try { + return await isBinInPath(gccTool, workspaceUri.fsPath, modifiedEnv); + } catch (error) { + Logger.errorNotify(`${tool} is not found in idf.customExtraPaths`, error); + return; + } +} + export function getToolchainToolName(idfTarget: string, tool: string = "gcc") { switch (idfTarget) { case "esp32": @@ -1076,7 +1091,7 @@ export async function startPythonReqsProcess( `"${pythonBinPath}" "${reqFilePath}" -r "${requirementsPath}"`, extensionContext.extensionPath, OutputChannel.init(), - { env: modifiedEnv } + { env: modifiedEnv, cwd: extensionContext.extensionPath } ); } diff --git a/templates/.vscode/launch.json b/templates/.vscode/launch.json index 6d2236f73..2511a38aa 100644 --- a/templates/.vscode/launch.json +++ b/templates/.vscode/launch.json @@ -1,6 +1,11 @@ { "version": "0.2.0", "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + }, { "type": "espidf", "name": "Launch", diff --git a/testFiles/testWorkspace/.vscode/launch.json b/testFiles/testWorkspace/.vscode/launch.json index 6d2236f73..2511a38aa 100644 --- a/testFiles/testWorkspace/.vscode/launch.json +++ b/testFiles/testWorkspace/.vscode/launch.json @@ -1,6 +1,11 @@ { "version": "0.2.0", "configurations": [ + { + "type": "gdbtarget", + "request": "attach", + "name": "Eclipse CDT GDB Adapter" + }, { "type": "espidf", "name": "Launch", diff --git a/webpack.config.js b/webpack.config.js index 54f08aa8c..5c8de8078 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,7 @@ const fs = require("fs"); const path = require("path"); const { VueLoaderPlugin } = require("vue-loader"); const webpack = require("webpack"); +const CopyWebpackPlugin = require('copy-webpack-plugin'); const packageConfig = JSON.parse( fs.readFileSync(path.join(__dirname, "package.json"), "utf8") @@ -50,8 +51,26 @@ const extensionConfig = { }, ], }, + { + test: /node-gyp-build\.js$/, + loader: 'string-replace-loader', + options: { + search: /path\.join\(dir, 'prebuilds'/g, + replace: "path.join(__dirname, 'prebuilds'", + } + } ], }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(__dirname, "./node_modules/@serialport/bindings-cpp/prebuilds"), + to: path.resolve(__dirname, "./dist/prebuilds") + } + ] + }), + ], resolve: { extensions: [".js", ".ts"], }, diff --git a/yarn.lock b/yarn.lock index a2e3efa5f..80c652da3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -283,16 +283,138 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@serialport/binding-mock@10.2.2": + version "10.2.2" + resolved "https://registry.yarnpkg.com/@serialport/binding-mock/-/binding-mock-10.2.2.tgz#d322a8116a97806addda13c62f50e73d16125874" + integrity sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw== + dependencies: + "@serialport/bindings-interface" "^1.2.1" + debug "^4.3.3" + +"@serialport/bindings-cpp@12.0.1": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@serialport/bindings-cpp/-/bindings-cpp-12.0.1.tgz#b7588a8b3e124e7679622ce980a7d8528e9f36a3" + integrity sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg== + dependencies: + "@serialport/bindings-interface" "1.2.2" + "@serialport/parser-readline" "11.0.0" + debug "4.3.4" + node-addon-api "7.0.0" + node-gyp-build "4.6.0" + +"@serialport/bindings-interface@1.2.2", "@serialport/bindings-interface@^1.2.1": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz#c4ae9c1c85e26b02293f62f37435478d90baa460" + integrity sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA== + +"@serialport/parser-byte-length@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-byte-length/-/parser-byte-length-12.0.0.tgz#18b1db5d1b3b9d8e1153eb2ab3975ac5445b844a" + integrity sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg== + +"@serialport/parser-cctalk@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-cctalk/-/parser-cctalk-12.0.0.tgz#f5c573b1ad2a9eed377aea9d70d264b36ca5dd5d" + integrity sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw== + +"@serialport/parser-delimiter@11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-delimiter/-/parser-delimiter-11.0.0.tgz#e830c6bb49723d4446131277dc3243b502d09388" + integrity sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g== + +"@serialport/parser-delimiter@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz#43d3687f982829cc9b48ee0b21f2de80d0f19778" + integrity sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw== + +"@serialport/parser-inter-byte-timeout@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-12.0.0.tgz#1436f36fac92c950d290744e8ce56b2273a61d08" + integrity sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w== + +"@serialport/parser-packet-length@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-packet-length/-/parser-packet-length-12.0.0.tgz#3b5b8b47b6971c03dbc90ba61c0b8c5ec8bb0798" + integrity sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ== + +"@serialport/parser-readline@11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-readline/-/parser-readline-11.0.0.tgz#c2c8c88e163d2abf7c0ffddbc1845336444e3454" + integrity sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA== + dependencies: + "@serialport/parser-delimiter" "11.0.0" + +"@serialport/parser-readline@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-readline/-/parser-readline-12.0.0.tgz#50e992004d7a84d5a12e0b016adb9021d3a72fbb" + integrity sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w== + dependencies: + "@serialport/parser-delimiter" "12.0.0" + +"@serialport/parser-ready@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-ready/-/parser-ready-12.0.0.tgz#193495e10c5a663029bce074d4f84cad173aab82" + integrity sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg== + +"@serialport/parser-regex@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-regex/-/parser-regex-12.0.0.tgz#ffbb2b113f3a50d7760fcdff5a4dd0f213ab8166" + integrity sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA== + +"@serialport/parser-slip-encoder@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-slip-encoder/-/parser-slip-encoder-12.0.0.tgz#362099d4cd170afe8583f1fa607176fd4fc14f1d" + integrity sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA== + +"@serialport/parser-spacepacket@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/parser-spacepacket/-/parser-spacepacket-12.0.0.tgz#347e34b0221f29eb252ebd341a0acfff920ad814" + integrity sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q== + +"@serialport/stream@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@serialport/stream/-/stream-12.0.0.tgz#047f97f780d92ddfc04303cb625e0f7e5a01a2bf" + integrity sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q== + dependencies: + "@serialport/bindings-interface" "1.2.2" + debug "4.3.4" + "@sindresorhus/is@^5.2.0": version "5.6.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.6.0.tgz#41dd6093d34652cddb5d5bdeee04eafc33826668" integrity sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g== +"@sindresorhus/merge-streams@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" + integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== + "@szmarczak/http-timer@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" @@ -370,7 +492,7 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== -"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -505,18 +627,23 @@ dependencies: "@vscode/debugprotocol" "1.64.0" -"@vscode/debugadapter@^1.53.0": - version "1.64.0" - resolved "https://registry.yarnpkg.com/@vscode/debugadapter/-/debugadapter-1.64.0.tgz#1aee3be252534ffd584fcad528afd4294a9b8674" - integrity sha512-XygE985qmNCzJExDnam4bErK6FG9Ck8S5TRPDNESwkt7i3OXqw5a3vYb7Dteyhz9YMEf7hwhFoT46Mjc45nJUg== +"@vscode/debugadapter@^1.65.0": + version "1.65.0" + resolved "https://registry.yarnpkg.com/@vscode/debugadapter/-/debugadapter-1.65.0.tgz#1a318aea805a86da8d497f3b80f4506bdb12e866" + integrity sha512-l9jdX0GFoFVAc7O4O8iVnCjO0pgxbx+wJJXCaYSuglGtYwMNcJdc7xm96cuVx4LWzSqneIjvjzbuzZtoVZhZzQ== dependencies: - "@vscode/debugprotocol" "1.64.0" + "@vscode/debugprotocol" "1.65.0" -"@vscode/debugprotocol@1.64.0", "@vscode/debugprotocol@^1.53.0": +"@vscode/debugprotocol@1.64.0": version "1.64.0" resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.64.0.tgz#f20d998b96474a8ca1aab868fcda08be38fa1f41" integrity sha512-Zhf3KvB+J04M4HPE2yCvEILGVtPixXUQMLBvx4QcAtjhc5lnwlZbbt80LCsZO2B+2BH8RMgVXk3QQ5DEzEne2Q== +"@vscode/debugprotocol@1.65.0", "@vscode/debugprotocol@^1.65.0": + version "1.65.0" + resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.65.0.tgz#304a9e0f4f2825a66db4647148d4b2ec6372f17e" + integrity sha512-ejerrPMBXzYms6Ks+Gb7cdXtdncmT0xwIKNsc0c/SxhEa0HVY5jdvLUegYE91p7CQJpCnXOD/r2CvViN8txLLA== + "@vscode/extension-telemetry@0.4.8": version "0.4.8" resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.4.8.tgz#bb67386ae7ba4d3505978243df91055d5e0cf950" @@ -844,11 +971,25 @@ agent-base@^7.0.2, agent-base@^7.1.0: dependencies: debug "^4.3.4" +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -859,6 +1000,16 @@ ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.9.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -1401,6 +1552,18 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +copy-webpack-plugin@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz#935e57b8e6183c82f95bd937df658a59f6a2da28" + integrity sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA== + dependencies: + fast-glob "^3.3.2" + glob-parent "^6.0.1" + globby "^14.0.0" + normalize-path "^3.0.0" + schema-utils "^4.2.0" + serialize-javascript "^6.0.2" + core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -1519,7 +1682,7 @@ d3-scale@^4.0.2: dependencies: d3-array "2 - 3" -debug@4, debug@^4.1.0, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1881,11 +2044,22 @@ eyes@0.1.x: resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ== -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -1896,6 +2070,13 @@ fastest-levenshtein@^1.0.12: resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -2073,13 +2254,20 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -2131,6 +2319,18 @@ glob@^7.0.3, glob@^7.0.6, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +globby@^14.0.0: + version "14.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.1.tgz#a1b44841aa7f4c6d8af2bc39951109d77301959b" + integrity sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ== + dependencies: + "@sindresorhus/merge-streams" "^2.1.0" + fast-glob "^3.3.2" + ignore "^5.2.4" + path-type "^5.0.0" + slash "^5.1.0" + unicorn-magic "^0.1.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -2335,6 +2535,11 @@ ignore@^5.1.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== +ignore@^5.2.4: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -2456,7 +2661,7 @@ is-generator-function@^1.0.7: dependencies: has-tostringtag "^1.0.0" -is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -2623,6 +2828,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stringify-safe@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -2875,7 +3085,12 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -micromatch@^4.0.0: +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.0, micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -3093,11 +3308,21 @@ node-abi@^3.3.0: dependencies: semver "^7.3.5" +node-addon-api@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" + integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== + node-addon-api@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== +node-gyp-build@4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + node-html-markdown@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/node-html-markdown/-/node-html-markdown-1.3.0.tgz#ef0b19a3bbfc0f1a880abb9ff2a0c9aa6bbff2a9" @@ -3351,6 +3576,11 @@ path-scurry@^1.10.2: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-type@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" + integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + pathval@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" @@ -3584,6 +3814,11 @@ qs@^6.5.1, qs@^6.9.1: dependencies: side-channel "^1.0.4" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -3668,6 +3903,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-alpn@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -3701,6 +3941,11 @@ responselike@^3.0.0: dependencies: lowercase-keys "^3.0.0" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -3722,6 +3967,13 @@ rollup@^3.27.1: optionalDependencies: fsevents "~2.3.2" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -3794,6 +4046,16 @@ schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +schema-utils@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + selenium-webdriver@^4.16.0: version "4.17.0" resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.17.0.tgz#f6c93a9df3e0543df7dc2329d81968af42845a7f" @@ -3827,13 +4089,33 @@ serialize-javascript@6.0.0: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" +serialport@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/serialport/-/serialport-12.0.0.tgz#136f0976042f57a2e99e886221a2109934531602" + integrity sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA== + dependencies: + "@serialport/binding-mock" "10.2.2" + "@serialport/bindings-cpp" "12.0.1" + "@serialport/parser-byte-length" "12.0.0" + "@serialport/parser-cctalk" "12.0.0" + "@serialport/parser-delimiter" "12.0.0" + "@serialport/parser-inter-byte-timeout" "12.0.0" + "@serialport/parser-packet-length" "12.0.0" + "@serialport/parser-readline" "12.0.0" + "@serialport/parser-ready" "12.0.0" + "@serialport/parser-regex" "12.0.0" + "@serialport/parser-slip-encoder" "12.0.0" + "@serialport/parser-spacepacket" "12.0.0" + "@serialport/stream" "12.0.0" + debug "4.3.4" + set-function-length@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" @@ -3911,6 +4193,11 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -3947,6 +4234,14 @@ stream-browserify@^3.0.0: inherits "~2.0.4" readable-stream "^3.5.0" +string-replace-loader@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-3.1.0.tgz#11ac6ee76bab80316a86af358ab773193dd57a4f" + integrity sha512-5AOMUZeX5HE/ylKDnEa/KKBqvlnFmRZudSOjVJHxhoJg9QYTwl1rECx7SLR8BBH7tfxb4Rp7EM2XVfQFxIhsbQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -4267,6 +4562,11 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -4318,6 +4618,11 @@ utf8-byte-length@^1.0.1: resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"