├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── GitHub2ADO.yml │ └── workflow.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS_GUIDELINES.md ├── README.md ├── THIRD_PARTY_NOTICE.md ├── assets ├── process-picker │ ├── refresh.svg │ └── refresh_inverse.svg ├── ros │ └── core-monitor │ │ └── style.css └── scripts │ └── ros2_launch_dumper.py ├── doc ├── debug-support.md └── spec │ └── debug-ros-nodes.md ├── languages ├── ros.msg.configuration.json └── syntaxes │ └── ros.msg.tmLanguage.json ├── media ├── documentation │ ├── debug-support │ │ ├── attach-to-cpp.gif │ │ ├── attach-to-python.gif │ │ ├── check-roscore-status.gif │ │ ├── create-attach-debug-config.gif │ │ ├── create-launch-debug-config.gif │ │ ├── launch-and-debug-nodes.gif │ │ └── ros2-launch-debug.gif │ ├── download-vsix-artifact.png │ ├── draft-release.png │ ├── git-fork.png │ ├── pipeline-manual-release.png │ └── spec │ │ └── debug-ros-nodes │ │ ├── architecture.png │ │ ├── attach-debug.png │ │ ├── debug-flow.png │ │ ├── execute-a-debug-configuration.png │ │ └── launch-debug.png └── icon.png ├── package-lock.json ├── package.json ├── src ├── build-tool │ ├── build-tool.ts │ ├── catkin-tools.ts │ ├── catkin.ts │ ├── colcon.ts │ ├── common.ts │ └── ros-shell.ts ├── cpp-formatter.ts ├── debugger │ ├── configuration │ │ ├── providers │ │ │ └── ros.ts │ │ └── resolvers │ │ │ ├── attach.ts │ │ │ ├── ros1 │ │ │ └── launch.ts │ │ │ └── ros2 │ │ │ ├── debug_launch.ts │ │ │ └── launch.ts │ ├── debug-session.ts │ ├── main.ts │ ├── manager.ts │ ├── process-picker │ │ ├── local-process-items-provider.ts │ │ ├── process-entry.ts │ │ ├── process-items-provider-impl-ps.ts │ │ ├── process-items-provider-impl-wmic.ts │ │ ├── process-items-provider.ts │ │ ├── process-picker.ts │ │ ├── process-quick-pick.ts │ │ └── utils.ts │ ├── requests.ts │ ├── utils.ts │ ├── vscode-cpptools.launch.json.d.ts │ ├── vscode-python.api.d.ts │ └── vscode-python.launch.json.d.ts ├── extension.ts ├── promise-fs.ts ├── ros │ ├── build-env-utils.ts │ ├── cli.ts │ ├── common │ │ └── unknown-ros.ts │ ├── ros.ts │ ├── ros1 │ │ ├── core-helper.ts │ │ ├── ros1.ts │ │ └── webview │ │ │ └── ros1_webview_main.ts │ ├── ros2 │ │ ├── daemon.ts │ │ ├── ros2-monitor.ts │ │ ├── ros2.ts │ │ └── webview │ │ │ └── ros2_webview_main.ts │ └── utils.ts ├── telemetry-helper.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts ├── urdfPreview │ ├── URDFPreviewPanel.ts │ ├── preview.ts │ └── previewManager.ts └── vscode-utils.ts ├── templates └── preview.html ├── tsconfig.json ├── tslint.json └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | (Please add appropriate labels) 11 | - [ ] Windows: (Version) 12 | - [ ] Linux: (Dist/Version) 13 | - [ ] ROS 1: Dist 14 | - [ ] ROS 2: Dist 15 | 16 | \ 17 | 18 | \ 19 | 20 | # what is the bug 21 | \ 22 | 23 | ## Repro steps 24 | \ 25 | 1. 26 | 2. 27 | 3. 28 | 29 | # expected behavior 30 | \ 31 | 32 | # additional context 33 | \ 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature] " 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **what is needed** 11 | \ 12 | 13 | **why is it needed** 14 | \ 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: "@types/node" 11 | versions: 12 | - 14.14.25 13 | - 15.0.0 14 | - dependency-name: "@types/vscode" 15 | versions: 16 | - 1.54.0 17 | - dependency-name: typescript 18 | versions: 19 | - 4.1.3 20 | - 4.1.4 21 | - 4.1.5 22 | - 4.2.2 23 | - 4.2.3 24 | - dependency-name: vscode-test 25 | versions: 26 | - 1.5.0 27 | - 1.5.1 28 | - dependency-name: "@types/mocha" 29 | versions: 30 | - 8.2.0 31 | - 8.2.1 32 | - dependency-name: mocha 33 | versions: 34 | - 8.2.1 35 | - 8.3.0 36 | - 8.3.1 37 | -------------------------------------------------------------------------------- /.github/workflows/GitHub2ADO.yml: -------------------------------------------------------------------------------- 1 | name: GitHub2ADO 2 | 3 | on: 4 | issues: 5 | types: 6 | [opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned] 7 | issue_comment: 8 | types: [created, edited, deleted] 9 | 10 | jobs: 11 | alert: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: danhellem/github-actions-issue-to-work-item@v2.1 15 | env: 16 | ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}" 17 | github_token: "${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}" 18 | ado_organization: "${{ secrets.ADO_ORGANIZATION_TOKEN }}" 19 | ado_project: "${{ secrets.ADO_PROJECT_TOKEN }}" 20 | ado_area_path: "${{ secrets.ADO_AREA_PATH_TOKEN }}" 21 | ado_wit: "Issue" 22 | ado_new_state: "New" 23 | ado_active_state: "Active" 24 | ado_close_state: "Closed" 25 | ado_bypassrules: false 26 | log_level: 100 27 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | paths-ignore: 4 | - "*.md" 5 | - "media/documentation/**" 6 | branches: 7 | - master 8 | pull_request: 9 | paths-ignore: 10 | - "*.md" 11 | - "media/documentation/**" 12 | release: 13 | types: 14 | - published 15 | 16 | jobs: 17 | build: 18 | strategy: 19 | matrix: 20 | os: [macos-latest, ubuntu-latest, windows-latest] 21 | fail-fast: false 22 | runs-on: ${{ matrix.os }} 23 | if: github.event_name == 'push' || github.event_name == 'pull_request' 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | - name: Install Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 18.x 31 | - run: npm install 32 | archive-vsix: 33 | runs-on: ubuntu-latest 34 | needs: [build] 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v2 38 | - name: Install Node.js 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: 18.x 42 | - run: npm install 43 | - name: Build VSIX package 44 | run: | 45 | npm install vsce -g 46 | npm run compile 47 | vsce package -o vscode-ros-dev.vsix 48 | - uses: actions/upload-artifact@v1 49 | if: github.event_name == 'push' 50 | with: 51 | name: vscode-ros-dev-vsix 52 | path: vscode-ros-dev.vsix 53 | publish-vsix: 54 | runs-on: ubuntu-latest 55 | if: github.event_name == 'release' && github.event.action == 'published' && github.repository == 'ms-iot/vscode-ros' 56 | steps: 57 | - name: Checkout 58 | uses: actions/checkout@v2 59 | - name: Install Node.js 60 | uses: actions/setup-node@v3 61 | with: 62 | node-version: 18.x 63 | - run: npm install 64 | - name: Build VSIX package 65 | run: | 66 | npm install @vscode/vsce -g 67 | npm run compile 68 | - name: Build Release VSIX package 69 | if: '!github.event.release.prerelease' 70 | run: | 71 | vsce package -o vscode-ros.vsix 72 | - name: Build Prerelease VSIX package 73 | if: 'github.event.release.prerelease' 74 | run: | 75 | vsce package --pre-release -o vscode-ros.vsix 76 | - name: Publish Release VSIX package 77 | if: '!github.event.release.prerelease' 78 | run: | 79 | vsce publish -p ${{secrets.MARKETPLACE_PUBLISHER_TOKEN}} 80 | - name: Publish Prerelease VSIX package 81 | if: 'github.event.release.prerelease' 82 | run: | 83 | vsce publish --pre-release -p ${{secrets.MARKETPLACE_PUBLISHER_TOKEN}} 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node.js 2 | out 3 | dist/** 4 | node_modules 5 | 6 | # vscode-test 7 | .vscode-test 8 | .vscode/c_cpp_properties.json 9 | .vscode/settings.json 10 | 11 | vscode-ros-dev.vsix -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 14 | "preLaunchTask": "npm: webpack" 15 | }, 16 | { 17 | "name": "Debugger", 18 | "type": "node", 19 | "request": "launch", 20 | "cwd": "${workspaceRoot}", 21 | "program": "${workspaceRoot}/out/src/debugger/main.js", 22 | "args": ["--server=4711"], 23 | "stopOnEntry": false, 24 | "sourceMaps": true, 25 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"], 26 | "preLaunchTask": "npm: watch" 27 | }, 28 | { 29 | "name": "Tests", 30 | "type": "extensionHost", 31 | "request": "launch", 32 | "runtimeExecutable": "${execPath}", 33 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], 34 | "stopOnEntry": false, 35 | "sourceMaps": true, 36 | "outFiles": ["${workspaceRoot}/out/test"], 37 | "preLaunchTask": "npm: watch" 38 | } 39 | ], 40 | "compounds": [ 41 | { 42 | "name": "Extension and Debugger", 43 | "configurations": ["Extension", "Debugger"] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | // https://github.com/microsoft/vscode-extension-samples/blob/master/helloworld-sample/.vscode/tasks.json 8 | "type": "npm", 9 | "script": "watch", 10 | "problemMatcher": "$tsc-watch", 11 | "isBackground": true, 12 | "presentation": { 13 | "reveal": "never" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | test/** 5 | src/** 6 | **/*.map 7 | node_modules/** 8 | .gitignore 9 | tsconfig.json 10 | webpack.config.js 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 0.9.3 3 | zero change release to prime the pre-release channel for future test releases. 4 | 5 | ## 0.9.2 6 | Hot fix for ROS1 Debugging 7 | 8 | ## 0.9.1 9 | * Revert change to filter XML and YAML files from launching. 10 | 11 | ## 0.9.0 12 | ## Thank you! 13 | This extension has crossed an impressive milestone - 500,000 Installs! 14 | 15 | ## New Features 16 | * Includes the ability to debug ROS2 launch files using the new debug_launch type. 17 | > Notes: 18 | This only debugs the launch file itself - it does not launch any ROS nodes. 19 | If your launch file does execute code, these may launch during debugging. 20 | 21 | * Smaller runtime, Faster Startup using WebPack 22 | * `.urdf` files are now processed with `xacro` 23 | * Fixes to ROS2 in a container 24 | 25 | ## What's Changed 26 | * Use Webpack to reduce loading times. by @ooeygui in https://github.com/ms-iot/vscode-ros/pull/607 27 | * Add the python analysis paths for ROS by @dthelegend in https://github.com/ms-iot/vscode-ros/pull/795 28 | * Bump mocha from 9.2.2 to 10.0.0 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/717 29 | * Bump @vscode/debugadapter from 1.56.1 to 1.57.0 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/766 30 | * Update attach.js to enable pretty-printing for "ros attach" mode by @zhh2005757 in https://github.com/ms-iot/vscode-ros/pull/815 31 | * Bump shell-quote from 1.7.3 to 1.7.4 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/811 32 | * Bump tslib from 2.4.0 to 2.4.1 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/823 33 | * Bump @types/mocha from 9.1.1 to 10.0.0 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/802 34 | * Bump typescript from 4.7.4 to 4.8.4 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/794 35 | * Bump portfinder from 1.0.28 to 1.0.32 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/774 36 | * Lamadio/0.9 fixes by @ooeygui in https://github.com/ms-iot/vscode-ros/pull/898 37 | * Bump @types/node from 17.0.45 to 18.15.7 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/897 38 | * Bump ts-loader from 9.3.1 to 9.4.2 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/831 39 | * Feature/debug ROS 2 launch files by @ooeygui in https://github.com/ms-iot/vscode-ros/pull/900 40 | * Bump webpack from 5.74.0 to 5.76.3 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/896 41 | * Bump typescript from 4.8.4 to 5.0.2 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/893 42 | * Bump webpack-cli from 4.10.0 to 5.0.1 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/833 43 | * Bump mocha and @types/mocha by @dependabot in https://github.com/ms-iot/vscode-ros/pull/837 44 | * Bump tslib from 2.4.1 to 2.5.0 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/866 45 | * Bump @vscode/debugadapter from 1.57.0 to 1.59.0 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/867 46 | * Bump @types/vscode from 1.66.0 to 1.69.0 by @dependabot in https://github.com/ms-iot/vscode-ros/pull/752 47 | 48 | ## New Contributors 49 | * @dthelegend made their first contribution in https://github.com/ms-iot/vscode-ros/pull/795 50 | * @zhh2005757 made their first contribution in https://github.com/ms-iot/vscode-ros/pull/815 51 | 52 | ## 0.8.4 53 | * makes 0.8.3 public 54 | 55 | ## 0.8.3 56 | * Use default workspace storage for IntelliSense db to avoid frequent writes to watched .vscode directory 57 | 58 | ## 0.8.2 59 | * Correct ROS1 launch command that does not contain arguments 60 | 61 | ## 0.8.1 62 | * Update Dependencies 63 | * Update Docs 64 | * Add Video Deep Dives 65 | * [645] Fix for launch ROS1 file parameters 66 | * [678] Ability to specify a startup script instead of inferring from distro 67 | * [671] Prevent spurreous output and stderr with output from preventing ROS2 launch file debugging 68 | 69 | ## 0.8.0 70 | * Update Dependencies 71 | * [#654] Ignore Scripts such as .sh, .bash, .bat, .ps1 and .cmd 72 | * [#655] Support Debugging on Jetson Nano by fixing the launch debugging python script 73 | * [#656] Fix Lifecycle node debugging, by addressing a hanging background thread. 74 | 75 | ## 0.7.0 76 | * Update 3rd party disclosure and attribution 77 | * Update Dependencies 78 | * [#544] Contribution by @vigiroux - Bogus Screen Size 79 | * [#587] Enable System Symbols 80 | * [#594] Improve IntelliSense by updating the C++ properties correctly 81 | * [#605] Support *Experimental* launch filtering - enable 'launch only' and 'debug only' Filters. 82 | * [#608] Reduce annoying dialogs from the ROS extension 83 | 84 | ## 0.6.9 85 | * [#429] Contribution by RyanDraves - Start ROS Core/Daemon if not running when launch debugging 86 | * [#435] Contribution by RyanDraves - Support ROS Test launch file debugging 87 | * [#470] Support VSCode Trusted Workspaces feature 88 | * [#476] Fixes to auto-launch 89 | * [#498] Support GDB Pretty Printing for Watch Window 90 | * [#497] No longer droping Log folders all over 91 | * [#499] ROS2 colcon Fixes, neutralize commands between ROS1 and ROS2 92 | * [#501] Fixes when using ROS in a container 93 | * Update many dependencies 94 | * Update documentation 95 | 96 | ## 0.6.8 97 | 98 | * [#443]https://github.com/ms-iot/vscode-ros/issues/443) URDF preview not working 99 | * Updated dependencies 100 | * Updated attribution 101 | 102 | ## 0.6.7 103 | 104 | * [#391](https://github.com/ms-iot/vscode-ros/pull/391) Launch debug args is optional. (by @seanyen) 105 | 106 | ## 0.6.6 107 | 108 | * [#372](https://github.com/ms-iot/vscode-ros/pull/372) Adding error messages on exceptions for better diagnosis (by @seanyen) 109 | * [#371](https://github.com/ms-iot/vscode-ros/pull/371) Adding the missing version for cpp properties. (by @seanyen) 110 | * [#368](https://github.com/ms-iot/vscode-ros/pull/368) [ROS2] Adding Initial ROS2 Launch Debug Support (by @seanyen) 111 | 112 | ## 0.6.5 113 | 114 | * [#365](https://github.com/ms-iot/vscode-ros/pull/365) [ros2] find all launch files (by @Hexafog) 115 | * [#310](https://github.com/ms-iot/vscode-ros/pull/310) Allow customization of build tool tasks (by @anton-matosov) 116 | * [#321](https://github.com/ms-iot/vscode-ros/pull/321) Have executable search follow symbolic links (by @nightduck) 117 | * [#304](https://github.com/ms-iot/vscode-ros/pull/304) Handle preLaunch task explicitly (by @anton-matosov) 118 | 119 | ## 0.6.4 120 | 121 | * [#241](https://github.com/ms-iot/vscode-ros/pull/241) Fix task provider name mismatch (by @humanoid2050) 122 | * [#262](https://github.com/ms-iot/vscode-ros/pull/262) Add error handling for ROS launch debug (by @ooeygui) 123 | * [#263](https://github.com/ms-iot/vscode-ros/pull/263) Fix URDF Preview not functional with vscode v1.47 (by @seanyen) 124 | 125 | ## 0.6.3 126 | 127 | * Enable `ros.rosdep` extension command. 128 | * Fix roslaunch C++ node debugging on Windows. 129 | 130 | ## 0.6.2 131 | 132 | * Maintenance release 133 | * Display `ROS` version and distro for status 134 | 135 | ## 0.6.1 136 | 137 | * Enable support for launch-debug a ROS node 138 | * Update environment sourcing in `ros.createTerminal` to work with user `.bashrc`, [#123](https://github.com/ms-iot/vscode-ros/pull/123) 139 | * Update extension to source workspace environment after a ROS build task 140 | * Fix task provider usage 141 | * Fix debug config provider upon initialiazing a `launch.json` file 142 | 143 | ## 0.6.0 144 | 145 | * Add support for ROS2 support 146 | * Add support for attach-debug a ROS node 147 | * Automate ROS distro selection 148 | * Fix `rosrun` and `roslaunch` command execution 149 | * Implementation task provider for `catkin_make_isolated` 150 | 151 | ## 0.5.0 152 | 153 | * Enable previewing URDF and Xacro files 154 | * Fix bugs in ROS core monitor 155 | 156 | ## 0.4.5 157 | 158 | * Require `vscode` 1.26 159 | * Enable launching and terminating `roscore` on Windows 160 | * Update ROS core monitor implementation with webview API 161 | * Fix `sourceRosAndWorkspace()` for workspaces built with `catkin_make_isolated` 162 | * Fix `findPackageFiles()` for Windows 163 | * Replace all `ROS master` instances with `ROS core` 164 | 165 | ## 0.3.0 166 | 167 | * Automatically add workspace package include dirs to the include path. 168 | * Fix debug configuration creation. 169 | 170 | ## 0.2.0 171 | 172 | * Require `vscode` 1.18 173 | * Add support for catkin tools alongside catkin_make (thanks to @JamesGiller). 174 | * Remove some unimplemented commands. 175 | * Add "ROS: Create Terminal" command. 176 | 177 | ## 0.1.0 178 | 179 | * Require `vscode` 1.14 180 | * Automatically discover catkin make tasks. 181 | * Don't error when no args are specified (#3). 182 | 183 | ## 0.0.1 184 | 185 | * Initial release. 186 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners are the default owners for everything in the repo. 2 | # Unless a later match takes precedence, @ms-iot/ros will be 3 | # requested for review when someone opens a pull request. 4 | * @ms-iot/ros 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository follows a simplified [Forking Workflow][forking_workflow] (an adaptation from the [Gitflow Workflow][gitflow_workflow]) to manage changes and branches. Detailed explanation could be found [here][maintainers_guidelines]. In practice, just follow these steps: 4 | 5 | 1. create your own fork of the repository 6 | 2. branch off from the latest `master` branch 7 | 3. finish changes in the feature branch 8 | 4. create pull request to the `master` branch and go through the review process 9 | 10 | Done! 11 | 12 | ## Working with a fork 13 | 14 | ![a typical git fork][fork_repo] 15 | 16 | For team-managed projects (like this one), even if you have the access to make changes directly through `git push`, it is still recommended to makes changes through pull requests so that all changes could be reviewed, verified, and clearly recorded as commits. To do that, follow the forking workflow used in most open-source projects. 17 | 18 | ### Creating a fork 19 | 20 | After creating a fork of a repository, make sure to [configure a remote][git_configure_remote] first so the fork can sync up with the remote repository. For example, when working on a fork from this repository, do this first: 21 | 22 | ```batch 23 | git remote add upstream https://github.com/ms-iot/vscode-ros 24 | ``` 25 | 26 | *Note: the remote name does not need to be `upstream`, we are just using `upstream` as an example* 27 | 28 | ### Syncing a fork 29 | 30 | Forks do not automatically sync with the original repository, to keep forks up-to-date, here are a few ways to accomplish that: 31 | 32 | 1. `fetch + merge` as described in GitHub's [syncing a fork guide][git_sync_fork] 33 | 34 | ```batch 35 | git fetch -p upstream 36 | git checkout 37 | git merge upstream/ 38 | ``` 39 | 40 | 2. `pull upstream` (`git pull` is a wrapper for `fetch + merge`) 41 | 42 | ```batch 43 | git checkout 44 | git pull upstream 45 | ``` 46 | 47 | It is important to know that the above commands will only update the local git copy, the remote of the fork will not be updated. To keep both the local and the remote repositories up-to-date, make sure to do a `git push` after syncing. 48 | 49 | ## Build Instructions 50 | 51 | Please follow the following instructions to set up a development environment for extension development. 52 | 53 | ### Dependencies 54 | 55 | As a TypeScript project, this extension has dependencies on: 56 | 57 | * [`Node.js`][nodejs] 58 | * `npm` ([distributed with `Node.js`][npmjs-get_npm]) 59 | 60 | After installing `node` (and `npm`), clone the repository. 61 | 62 | ```bash 63 | git clone https://github.com/ms-iot/vscode-ros 64 | ``` 65 | 66 | ### Build 67 | 68 | It is recommended to sync with upstream and update dependencies by running [`npm ci`][npmjs-ci] before making changes. 69 | 70 | *It is recommended to use `npm ci` instead of [`npm install`][npmjs-install]. 71 | This is because `npm ci` consumes `package-lock.json` to ensure the same version is used for all nested dependencies.* 72 | 73 | ```bash 74 | cd vscode-ros 75 | # sync with upstream 76 | 77 | npm ci 78 | code . 79 | ``` 80 | 81 | You can now go to the Debug viewlet (`Ctrl+Shift+D`), select `Extension`, and then hit run (`F5`). 82 | 83 | This will open a new VS Code window which will have the title `[Extension Development Host]`. 84 | In this window, open any ROS workspace. 85 | 86 | ### Debug 87 | 88 | In the original VS Code window, you can add breakpoints which will be hit when they are executed. 89 | 90 | If you make edits in the extension `.ts` files, just reload/restart (`Ctrl+Shift+F5`) the `[Extension Development Host]` instance of Code to load in the new extension code. Similarly, you could also execute the `Developer: Reload Window` command through command palette in the `[Extension Development Host]` instance. 91 | 92 | The debugging instance will automatically reattach. 93 | 94 | ### Running the tests 95 | 96 | To run the tests locally, open the Debug viewlet (`Ctrl+Shift+D`), select `Tests`, then hit run (`F5`). 97 | 98 | ### Sideloading the extension 99 | 100 | After making changes locally, you might want to test it end to end. 101 | To do this, you can sideload the extension. 102 | This can be done by [packaging the extension][vscode-package_extension] and loading the `.vsix` directly. 103 | In the workspace, follow these steps: 104 | 105 | ```batch 106 | npm install -g vsce 107 | npm ci 108 | vsce package 109 | ``` 110 | 111 | Then in any VS Code instance, run the command `Extensions: Install from VSIX...`. 112 | Choose the `.vsix` file generated in the previous step 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | ## Releasing a new version 121 | 122 | Please check release instructions in [maintainers' guidelines][maintainers_guidelines]. 123 | 124 | ## Microsoft Open Source Code of Conduct 125 | 126 | This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com. 127 | 128 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 129 | 130 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 131 | 132 | 133 | [fork_repo]: /media/documentation/git-fork.png 134 | [maintainers_guidelines]: MAINTAINERS_GUIDELINES.md 135 | 136 | 137 | [forking_workflow]: https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow 138 | [git_configure_remote]: https://help.github.com/en/articles/configuring-a-remote-for-a-fork 139 | [git_sync_fork]: https://help.github.com/en/articles/syncing-a-fork 140 | [gitflow_workflow]: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow 141 | [nodejs]: https://nodejs.org 142 | [npmjs-get_npm]: https://www.npmjs.com/get-npm 143 | [npmjs-ci]: https://docs.npmjs.com/cli/ci 144 | [npmjs-install]: https://docs.npmjs.com/cli/install 145 | [vscode-package_extension]: https://code.visualstudio.com/api/working-with-extensions/publishing-extension#packaging-extensions 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MAINTAINERS_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Guidelines for Maintainers 2 | 3 | ## Index 4 | 5 | - [Repository Policies](#repository-policies) 6 | - [Project Workflow](#project-workflow) 7 | - [Release Instructions](#release-instructions) 8 | 9 | ## Repository Policies 10 | 11 | Please follow these principles for this repository: 12 | 13 | - pull requests require 2+ approvals 14 | - always work in forks 15 | - do not merge your own changes 16 | - follow [release instructions](#release-instructions) 17 | 18 | ### Working with this repository 19 | 20 | Here are a few recommendations for maintainers of this project: 21 | 22 | - While this project is created as a fork from the [original vscode-ros project][ajshort_vscode-ros], please **do not** merge `upstream/master` and push (unless planned). 23 | - Please **do not** alter commit history unless it is necessary and everyone working on the project is notified: 24 | - **do not** use `git rebase` 25 | - **do not** use `git reset --hard` to revert to any commit earlier than current `HEAD` 26 | - try to avoid calling `git push --force` 27 | - Please **try not to** directly push to `origin`, work with forks and merge changes through pull requests. 28 | - Only use tags to track releases. 29 | 30 | ## Project workflow 31 | 32 | This repository follows a simplified [Forking Workflow][forking_workflow] (an adaptation from the [Gitflow Workflow][gitflow_workflow]) to manage changes and branches. 33 | 34 | 1. the Gitflow Workflow works with 5 types of branches: 35 | - `master`: combined with tags to track all releases 36 | - `dev`: for current codebase development 37 | - `release`: branches off from `dev` for release-related cleanup and udpate 38 | - `feature`: branches off from `dev` for feature development 39 | - `hotfix`: branches off from `master` for product code fix 40 | 2. the Forking Workflow is based on the Gitflow Workflow yet most of the times without `feature` branches because: 41 | - `feature` branches are hosted in the developer's own fork, the centralized repository would not be hosting these branches 42 | 3. this repository also does not use the `master` branch (the `master` branch in this repository acts as the `dev` branch) because: 43 | - tags are adequate for tracking each release 44 | - `hotfix` branches can branch off from the latest tag instead of the `master` branch (this is manageable when the project does not have too many releases) 45 | 46 | ## Release Instructions 47 | 48 | ### Versioning 49 | 50 | Versioning of this extension follows the [SemVer guidelines][semver_guidelines]. 51 | 52 | > Given a version number `MAJOR.MINOR.PATCH`, increment the: 53 | > 54 | > - `MAJOR` version when you make incompatible API changes, 55 | > - `MINOR` version when you add functionality in a backwards-compatible manner, and 56 | > - `PATCH` version when you make backwards-compatible bug fixes. 57 | > 58 | > Additional labels for pre-release and build metadata are available as extensions to the `MAJOR.MINOR.PATCH` format. 59 | 60 | This project is not expected to have an insider's channel, so there are just some very simple guidelines to follow in practice: 61 | 62 | 1. when any change is introduced into the `master` branch after the existing release (at this point, the version number in the `master` branch should be ``), update the version number in the `master` branch to `-dev` 63 | 2. after the `master` branch has been reviewed and is ready to be released, update the version number to `` and release 64 | - a newer version of the extension should be published as soon as the version number becomes `` 65 | 3. then, go to step 1. 66 | 67 | Reasoning: 68 | 69 | 1. VS Code extension marketplace hosts only the latest version; when there is no insider's channel, there is only 1 public version (the latest version) 70 | 2. extensions can only be published with numeric SemVer versions, so no pre-release fields for final releases 71 | 3. in order for packages installed from `.vsix` published from the [vscode-ros.ci pipeline][vscode-ros.ci] to receive updates to the final release on the VS Code extension marketplace, the version number shipped in the `.vsix` package must be smaller. Therefore, the version numbers need to have the pre-release field (`-dev`) 72 | 4. since there is no insider's channel, which means pre-release builds installed from `.vsix` will not receive auto-update to another pre-release build, there is no further versioning for the pre-release fields (no `-alpha`, `-beta`, `-rc1`, `-rc2`, etc.) 73 | 74 | ### Publishing a release 75 | 76 | #### Testing before Release 77 | 78 | Before preparing a release, you should check the health of this extension, for example, 79 | 80 | 1. Check the latest CI status: 81 | ![.github/workflows/workflow.yml](https://github.com/ms-iot/vscode-ros/workflows/.github/workflows/workflow.yml/badge.svg?event=push) 82 | 83 | 2. Run through the basic scenarios manually on Linux and Windows environments. 84 | - Start, terminate, and monitor ROS core 85 | 1. launch ROS core monitor with `ros.showMasterStatus` 86 | 2. start a new ROS core process in background with `ros.startCore` 87 | 3. check if ROS core monitor shows parameters and nodes correctly 88 | 4. termintate the ROS core process with `ros.stopCore` 89 | 5. check if ROS core monitor shows "offline" 90 | - Create a terminal with ROS environment sourced 91 | 1. start a new ROS terminal with `ros.createTerminal` 92 | 2. check if ROS environment variables are properly configured 93 | - Execute a ROS executable 94 | 1. start a ROS core process (in another terminal or with `ros.startCore`) 95 | 2. execute `ros.rosrun` and choose an executable 96 | 3. check if the executable gets launched 97 | - Execute a ROS launch file 98 | 1. execute `ros.roslaunch` and choose a launch file 99 | 2. check if the launch file gets launched 100 | - Execute URDF preview on a URDF file. 101 | - Configure ROS launch debug configuration, set breakpoints and start debugging. 102 | 103 | #### Preparing a Release 104 | 105 | For each release, we will need to bump the version number and update the documents accordingly. 106 | 107 | The following will be touched: 108 | 109 | - update `README.md` 110 | - update `CHANGELOG.md` 111 | - update version number in `package.json` and `package-lock.json` 112 | 113 | > You can find an example commit [here](https://github.com/ms-iot/vscode-ros/commit/3091180d319d2ca87736cd50c1293dd26151a0b8). 114 | 115 | #### Authorizing a release (through GitHub releases) 116 | 117 | The release process is integrated and automated with [GitHub releases](https://docs.github.com/en/github/administering-a-repository/about-releases). Here are 3-easy steps to kick off one. 118 | 119 | 1. [Draft a new release](https://github.com/ms-iot/vscode-ros/releases/new). 120 | 121 | 2. Fill the version number and release note. (Keep the target branch as `master`.) 122 | ![](/media/documentation/draft-release.png) 123 | 124 | 3. Click `Publish release`. 125 | 126 | Once it is kicked off, GitHub release will automatically tag your latest commit from the `master` and a GitHub action will automatically be triggered to build and publish the extension to VS Code extension marketplace. For example, here is [one release run](https://github.com/ms-iot/vscode-ros/actions/runs/580197093) in the past. 127 | 128 | #### Post-release Activities 129 | 130 | To get `master` ready for the next release, it is encouraged to update the version number immediately after a release is published. You can find an example commit [here](https://github.com/ms-iot/vscode-ros/commit/3fd13ba1def4f0eee3a0fc9e0e58db7558e119a3). 131 | 132 | 133 | 134 | [ajshort_vscode-ros]: https://github.com/ajshort/vscode-ros 135 | [forking_workflow]: https://www.atlassian.com/git/tutorials/comparing-workflows/forking-workflow 136 | [git_tagging]: https://git-scm.com/book/en/v2/Git-Basics-Tagging 137 | [gitflow_workflow]: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow 138 | [semver_guidelines]: https://semver.org/#semantic-versioning-specification-semver 139 | [vscode-ros.ci]: https://github.com/ms-iot/vscode-ros/actions 140 | -------------------------------------------------------------------------------- /assets/process-picker/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/process-picker/refresh_inverse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/ros/core-monitor/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-right: 20px; 3 | } 4 | 5 | table { 6 | border-collapse: collapse; 7 | width: 100%; 8 | } 9 | 10 | table thead { 11 | background: var(--vscode-sideBarSectionHeader-background); 12 | } 13 | 14 | table th { 15 | text-align: left; 16 | } 17 | 18 | table td, table th { 19 | padding: 6px; 20 | } 21 | 22 | table tbody tr { 23 | border-bottom: 1px solid var(--vscode-sideBarSectionHeader-background); 24 | } 25 | 26 | table tbody tr:hover { 27 | background: var(--vscode-sideBarSectionHeader-background); 28 | } 29 | 30 | #parameters .name { 31 | font-weight: bold; 32 | } 33 | 34 | #parameters ul { 35 | padding-left: 20px; 36 | } 37 | -------------------------------------------------------------------------------- /assets/scripts/ros2_launch_dumper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | import asyncio 5 | import argparse 6 | import os 7 | import sys 8 | import subprocess 9 | from io import StringIO 10 | from typing import cast 11 | from typing import Dict 12 | from typing import List 13 | from typing import Text 14 | from typing import Tuple 15 | from typing import Union 16 | 17 | from ament_index_python.packages import get_package_prefix 18 | from ament_index_python.packages import PackageNotFoundError 19 | from ros2launch.api import get_share_file_path_from_package 20 | from ros2launch.api import is_launch_file 21 | from ros2launch.api import launch_a_launch_file 22 | from ros2launch.api import LaunchFileNameCompleter 23 | from ros2launch.api import MultipleLaunchFilesError 24 | from ros2launch.api import print_a_launch_file 25 | from ros2launch.api import print_arguments_of_launch_file 26 | import launch 27 | from launch.launch_description_sources import get_launch_description_from_any_launch_file 28 | from launch.utilities import is_a 29 | from launch.utilities import normalize_to_list_of_substitutions 30 | from launch.utilities import perform_substitutions 31 | from launch import Action 32 | from launch.actions import ExecuteProcess 33 | from launch.actions import IncludeLaunchDescription 34 | from launch.actions import DeclareLaunchArgument 35 | from launch.launch_context import LaunchContext 36 | from launch_ros.actions import PushRosNamespace 37 | from launch_ros.ros_adapters import get_ros_adapter 38 | 39 | 40 | def parse_launch_arguments(launch_arguments: List[Text]) -> List[Tuple[Text, Text]]: 41 | """Parse the given launch arguments from the command line, into list of tuples for launch.""" 42 | parsed_launch_arguments = {} # type: ignore; 3.7+ dict is ordered. 43 | for argument in launch_arguments: 44 | count = argument.count(':=') 45 | if count == 0 or argument.startswith(':=') or (count == 1 and argument.endswith(':=')): 46 | raise RuntimeError( 47 | "malformed launch argument '{}', expected format ':='" 48 | .format(argument)) 49 | name, value = argument.split(':=', maxsplit=1) 50 | parsed_launch_arguments[name] = value # last one wins is intentional 51 | return parsed_launch_arguments.items() 52 | 53 | 54 | def find_files(file_name): 55 | search_results = [file_name] 56 | if os.name == 'nt': 57 | output = subprocess.Popen(['where', file_name], stdout=subprocess.PIPE, shell=True).communicate()[0] 58 | output = output.decode() 59 | search_results = output.split(os.linesep) 60 | else: 61 | # Do we need this on Linux? 62 | pass 63 | return search_results[0] 64 | 65 | 66 | if __name__ == "__main__": 67 | parser = argparse.ArgumentParser(description='Process some integers.') 68 | arg = parser.add_argument( 69 | 'launch_full_path', 70 | help='Full file path to the launch file') 71 | arg = parser.add_argument( 72 | 'launch_arguments', 73 | nargs='*', 74 | help="Arguments to the launch file; ':=' (for duplicates, last one wins)") 75 | 76 | args = parser.parse_args() 77 | 78 | path = None 79 | launch_arguments = [] 80 | 81 | if os.path.exists(args.launch_full_path): 82 | path = args.launch_full_path 83 | else: 84 | raise RuntimeError('No launch file supplied') 85 | 86 | launch_arguments.extend(args.launch_arguments) 87 | parsed_launch_arguments = parse_launch_arguments(launch_arguments) 88 | 89 | launch_description = launch.LaunchDescription([ 90 | launch.actions.IncludeLaunchDescription( 91 | launch.launch_description_sources.AnyLaunchDescriptionSource( 92 | path 93 | ), 94 | launch_arguments=parsed_launch_arguments, 95 | ), 96 | ]) 97 | walker = [] 98 | walker.append(launch_description) 99 | ros_specific_arguments: Dict[str, Union[str, List[str]]] = {} 100 | context = LaunchContext(argv=launch_arguments) 101 | context._set_asyncio_loop(asyncio.get_event_loop()) 102 | 103 | try: 104 | # * Here we mimic the run loop inside launch_service, 105 | # but without actually kicking off the processes. 106 | # * Traverse the sub entities by DFS. 107 | # * Shadow the stdout to avoid random print outputs. 108 | mystring = StringIO() 109 | sys.stdout = mystring 110 | while walker: 111 | entity = walker.pop() 112 | visit_future = entity.visit(context) 113 | if visit_future is not None: 114 | visit_future.reverse() 115 | walker.extend(visit_future) 116 | 117 | 118 | if is_a(entity, ExecuteProcess): 119 | typed_action = cast(ExecuteProcess, entity) 120 | if typed_action.process_details is not None: 121 | sys.stdout = sys.__stdout__ 122 | # Prefix this with a tab character so the caller knows this is a line to be processed. 123 | commands = ['\t'] 124 | for cmd in typed_action.process_details['cmd']: 125 | if cmd.strip(): 126 | commands.extend(['"{}"'.format(cmd.strip())]) 127 | if os.sep not in typed_action.process_details['cmd'][0]: 128 | commands[0] = '"{}"'.format(find_files(typed_action.process_details['cmd'][0])) 129 | print(' '.join(commands)) 130 | sys.stdout = mystring 131 | 132 | # Lifecycle node support 133 | # https://github.com/ms-iot/vscode-ros/issues/632 134 | # Lifecycle nodes use a long running future to monitor the state of the nodes. 135 | # cancel this task, so that we can shut down the executor in ros_adapter 136 | async_future = entity.get_asyncio_future() 137 | if async_future is not None: 138 | async_future.cancel() 139 | 140 | except Exception as ex: 141 | print(ex, file=sys.stderr) 142 | 143 | # Shutdown the ROS Adapter 144 | # so that long running tasks shut down correctly in the debug bootstrap scenario. 145 | # https://github.com/ms-iot/vscode-ros/issues/632 146 | ros_adapter = get_ros_adapter(context) 147 | if ros_adapter is not None: 148 | ros_adapter.shutdown() 149 | -------------------------------------------------------------------------------- /doc/debug-support.md: -------------------------------------------------------------------------------- 1 | # Debug ROS Nodes 2 | 3 | One of the key goals of `vscode-ros` is to provide a streamlined debugging experience for ROS nodes. 4 | To achieve this, this extension aims to help developers utilize the debugging capabilities provided by Visual Studio Code. 5 | This document covers instructions of how to use such functionalities. 6 | 7 | Read more about the design and related discussions on the debugging functionalities in our [design document][spec_debug_ros_nodes]. 8 | 9 | ## Attach 10 | 11 | `vscode-ros` enables a bootstrapped debugging experience for debugging a ROS (Python or C++) node by attaching to the process. 12 | 13 | To get started, create a `ros`-type debug configuration with an `attach` request: (use Ctrl-Space to bring up the autocomplete dropdown) 14 | 15 | ![create attach debug configuration][create_attach_debug_configuration] 16 | 17 | ### Attaching to a Python node 18 | 19 | ![attach to a python node][attach_to_python] 20 | 21 | ### Attaching to a C++ node 22 | 23 | ![attach to a cpp node][attach_to_cpp] 24 | 25 | ## Launch 26 | 27 | `vscode-ros` enables a streamlined debugging experience for debugging a ROS (Python or C++) node in a ROS launch file similar to a native debug flow. 28 | 29 | To get started, create a `ros`-type debug configuration with a `launch` request: 30 | 31 | ![create launch debug configuration][create_launch_debug_configuration] 32 | 33 | ### Prerequisite 34 | 35 | There needs to be a running instance of `rosmaster`. 36 | The launch-debug flow provided by `vscode-ros` will not spawn a `rosmaster`. 37 | 38 | ![check roscore status][check_roscore_status] 39 | 40 | ### Launch and debug Python and C++ nodes 41 | 42 | ![launch and debug Python and C++ nodes][launch_and_debug_nodes] 43 | 44 | ### Use tasks to automatically build before starting debug session 45 | 46 | The first thing you need to do is to create build task for your package(s) with enabled debug symbols. 47 | In the example below you can see a `catkin_make` build task that passes additional `-DCMAKE_BUILD_TYPE=Debug` argument that switches build to use `Debug` configuration, which is the most suitable configuration for debugging, because it has 0 optimization level and includes debug symbols. Another option is to use `-DCMAKE_BUILD_TYPE=RelWithDebInfo` that also enables debug symbols, but uses `Release` settings for everything else and has optimizations enabled. `RelWithDebInfo` might be a go to build configuration for Windows users in order to avoid slowness of the debug CRT on Windows OS. You can read more about [CMAKE_BUILD_TYPE here][stackoverflow-cmake_build_type] 48 | 49 | **Note: you might need to remove the old `build` folder to force rebuild in new configuraiton.** 50 | 51 | ```json5 52 | { 53 | "version": "2.0.0", 54 | "tasks": [ 55 | { 56 | "label": "make_debug", 57 | "type": "catkin_make", 58 | "args": [ 59 | "--directory", 60 | "${workspaceFolder}", 61 | "-DCMAKE_BUILD_TYPE=Debug", // This extra argument enables built with debug symbols 62 | ], 63 | "problemMatcher": [ 64 | "$catkin-gcc" 65 | ], 66 | "group": { 67 | "kind": "build", 68 | "isDefault": true 69 | }, 70 | }, 71 | ] 72 | } 73 | ``` 74 | 75 | The next step would be to configure `.vscode/launch.json` and customize `preLaunchTask` to use `make_debug` task we created above. 76 | 77 | ```json5 78 | { 79 | "version": "0.2.0", 80 | "configurations": [ 81 | { 82 | "name": "ROS: Launch", 83 | "type": "ros", 84 | "request": "launch", 85 | "target": "${workspaceFolder}/launch/some.launch", // <<< Configure path to your launch file 86 | "preLaunchTask": "make_debug", // <<< This is the task that will run before debugging starts 87 | } 88 | ] 89 | } 90 | 91 | ``` 92 | 93 | 94 | 95 | 96 | ### Use tasks to automatically build and start rosmaster 97 | 98 | **This is current BLOCKED BY VSCode Bug [70283][ms-vscode.background_bug]. This bug will prevent the second debugging session from starting if roscore background task is already running** 99 | 100 | This section continues setup that was described [above](#build_tasks), so please complete that section and ensure you can build and debug with manually started roscore 101 | 102 | We are going to define a new task named `make_debug_and_core` that is going to start both `make_debug` and `roscore: roscore` tasks. `roscore: roscore` is a background task that will continue running even after debuging session is over 103 | 104 | ```json5 105 | { 106 | "version": "2.0.0", 107 | "tasks": [ 108 | /// ... `make_debug` task definition as before 109 | { 110 | "label": "make_debug_and_core", 111 | "dependsOn": [ 112 | "make_debug", 113 | "roscore: roscore", // This task is provided by vscode-ros 114 | ] 115 | }, 116 | ] 117 | } 118 | ``` 119 | 120 | The next step would be to switch `preLaunchTask` to use `make_debug_and_core` task we created above. 121 | 122 | ```json5 123 | { 124 | "version": "0.2.0", 125 | "configurations": [ 126 | { 127 | // ... same as before 128 | "preLaunchTask": "make_debug_and_core", 129 | } 130 | ] 131 | } 132 | 133 | ``` 134 | 135 | ## Note 136 | 137 | 1. Debugging functionality provided by `vscode-ros` has dependencies on VS Code’s [C++][ms-vscode.cpptools] and [Python][ms-python.python] extensions, and those have dependencies on the version of VS Code. To ensure everything works as expected, please make sure to have everything up-to-date. 138 | 2. To debug a C++ executable, please make sure the binary is [built with debug symbols][ros_answers_debug_symbol] (e.g. `-DCMAKE_BUILD_TYPE=RelWithDebInfo`, read more about [CMAKE_BUILD_TYPE here][stackoverflow-cmake_build_type]). 139 | 3. To use VS Code's C++ extension with MSVC on Windows, please make sure the VS Code instance is launched from a Visual Studio command prompt. 140 | 141 | 142 | [create_attach_debug_configuration]: ../media/documentation/debug-support/create-attach-debug-config.gif 143 | [attach_to_cpp]: ../media/documentation/debug-support/attach-to-cpp.gif 144 | [attach_to_python]: ../media/documentation/debug-support/attach-to-python.gif 145 | [create_launch_debug_configuration]: ../media/documentation/debug-support/create-launch-debug-config.gif 146 | [check_roscore_status]: ../media/documentation/debug-support/check-roscore-status.gif 147 | [launch_and_debug_nodes]: ../media/documentation/debug-support/launch-and-debug-nodes.gif 148 | 149 | [spec_debug_ros_nodes]: ./spec/debug-ros-nodes.md 150 | 151 | 152 | [ros_answers_debug_symbol]: https://answers.ros.org/question/200155/how-to-debug-executable-built-with-catkin_make-without-roslaunch/ 153 | 154 | [ms-python.python]: https://marketplace.visualstudio.com/items?itemName=ms-python.python 155 | [ms-vscode.cpptools]: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools 156 | [ms-vscode.background_bug]: https://github.com/microsoft/vscode/issues/70283 157 | [stackoverflow-cmake_build_type]: https://stackoverflow.com/a/59314670/888545 158 | -------------------------------------------------------------------------------- /languages/ros.msg.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["[", "]"] 7 | ], 8 | "autoClosingPairs": [ 9 | { "open": "[", "close": "]", "notIn": ["comment"] } 10 | ], 11 | "surroundingPairs": [ 12 | ["[", "]"] 13 | ] 14 | } -------------------------------------------------------------------------------- /languages/syntaxes/ros.msg.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ros.msg", 3 | "scopeName": "source.ros.msg", 4 | "fileTypes": [ 5 | "action", 6 | "msg", 7 | "srv" 8 | ], 9 | "patterns": [ 10 | { 11 | "name": "meta.field.msg", 12 | "begin": "^\\s*(string)\\b", 13 | "beginCaptures": { 14 | "1": { 15 | "name": "storage.type.msg" 16 | } 17 | }, 18 | "end": "$", 19 | "patterns": [ 20 | { 21 | "begin": "=", 22 | "end": "$", 23 | "contentName": "string.unquoted.msg" 24 | }, 25 | { 26 | "include": "#field" 27 | } 28 | ] 29 | }, 30 | { 31 | "name": "meta.field.msg", 32 | "begin": "^\\s*([/\\w]+)\\b", 33 | "beginCaptures": { 34 | "1": { 35 | "patterns": [ 36 | { 37 | "include": "#types" 38 | } 39 | ] 40 | } 41 | }, 42 | "end": "$", 43 | "patterns": [ 44 | { 45 | "include": "#field" 46 | } 47 | ] 48 | }, 49 | { 50 | "include": "#comments" 51 | } 52 | ], 53 | "repository": { 54 | "field": { 55 | "patterns": [ 56 | { 57 | "include": "#comments" 58 | }, 59 | { 60 | "include": "#identifier" 61 | }, 62 | { 63 | "include": "#numeric-constant" 64 | }, 65 | { 66 | "begin": "=", 67 | "end": "$", 68 | "patterns": [ 69 | { 70 | "include": "#boolean-constant" 71 | }, 72 | { 73 | "include": "#numeric-constant" 74 | }, 75 | { 76 | "include": "#comments" 77 | } 78 | ] 79 | } 80 | ] 81 | }, 82 | "comments": { 83 | "match": "#.*", 84 | "name": "comment.line.number-sign.msg" 85 | }, 86 | "types": { 87 | "patterns": [ 88 | { 89 | "match": "\\b(bool|u?int(8|16|32|64)|float(32|64)|string|time|duration)\\b", 90 | "name": "storage.type.msg" 91 | }, 92 | { 93 | "match": "\\b(byte|char)\\b", 94 | "name": "invalid.deprecated.msg" 95 | }, 96 | { 97 | "match": "\\b[a-zA-Z]\\w*(\\/[a-zA-Z]\\w*)?\\b", 98 | "name": "support.class" 99 | } 100 | ] 101 | }, 102 | "identifier": { 103 | "match": "\\b[a-zA-Z]\\w+\\b", 104 | "name": "variable.parameter" 105 | }, 106 | "boolean-constant": { 107 | "match": "\\b(True|False)\\b", 108 | "name": "constant.language.msg" 109 | }, 110 | "numeric-constant": { 111 | "match": "\\b[+-]?[0-9]*\\.?[0-9]+([eE][+-]?\\d+)?\\b", 112 | "name": "constant.numeric.msg" 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /media/documentation/debug-support/attach-to-cpp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/attach-to-cpp.gif -------------------------------------------------------------------------------- /media/documentation/debug-support/attach-to-python.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/attach-to-python.gif -------------------------------------------------------------------------------- /media/documentation/debug-support/check-roscore-status.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/check-roscore-status.gif -------------------------------------------------------------------------------- /media/documentation/debug-support/create-attach-debug-config.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/create-attach-debug-config.gif -------------------------------------------------------------------------------- /media/documentation/debug-support/create-launch-debug-config.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/create-launch-debug-config.gif -------------------------------------------------------------------------------- /media/documentation/debug-support/launch-and-debug-nodes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/launch-and-debug-nodes.gif -------------------------------------------------------------------------------- /media/documentation/debug-support/ros2-launch-debug.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/debug-support/ros2-launch-debug.gif -------------------------------------------------------------------------------- /media/documentation/download-vsix-artifact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/download-vsix-artifact.png -------------------------------------------------------------------------------- /media/documentation/draft-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/draft-release.png -------------------------------------------------------------------------------- /media/documentation/git-fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/git-fork.png -------------------------------------------------------------------------------- /media/documentation/pipeline-manual-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/pipeline-manual-release.png -------------------------------------------------------------------------------- /media/documentation/spec/debug-ros-nodes/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/spec/debug-ros-nodes/architecture.png -------------------------------------------------------------------------------- /media/documentation/spec/debug-ros-nodes/attach-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/spec/debug-ros-nodes/attach-debug.png -------------------------------------------------------------------------------- /media/documentation/spec/debug-ros-nodes/debug-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/spec/debug-ros-nodes/debug-flow.png -------------------------------------------------------------------------------- /media/documentation/spec/debug-ros-nodes/execute-a-debug-configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/spec/debug-ros-nodes/execute-a-debug-configuration.png -------------------------------------------------------------------------------- /media/documentation/spec/debug-ros-nodes/launch-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/documentation/spec/debug-ros-nodes/launch-debug.png -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ms-iot/vscode-ros/b216c0d6271d40907c70b2d8f9fe6c0195d526d7/media/icon.png -------------------------------------------------------------------------------- /src/build-tool/build-tool.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from "path"; 5 | import * as vscode from "vscode"; 6 | 7 | import * as extension from "../extension"; 8 | import * as pfs from "../promise-fs"; 9 | import * as telemetry from "../telemetry-helper"; 10 | import * as catkin from "./catkin"; 11 | import * as catkin_tools from "./catkin-tools"; 12 | import * as colcon from "./colcon"; 13 | 14 | export abstract class BuildTool { 15 | public static current: BuildTool; 16 | public static registerTaskProvider(): vscode.Disposable[] { 17 | return this.current._registerTaskProvider(); 18 | } 19 | 20 | public static async createPackage(context: vscode.ExtensionContext) { 21 | const reporter = telemetry.getReporter(); 22 | reporter.sendTelemetryCommand(extension.Commands.CreateCatkinPackage); 23 | return this.current._createPackage(); 24 | } 25 | 26 | protected abstract _registerTaskProvider(): vscode.Disposable[]; 27 | protected abstract _createPackage(): Promise; 28 | } 29 | 30 | // tslint:disable-next-line: max-classes-per-file 31 | class NotImplementedBuildTool extends BuildTool { 32 | protected _registerTaskProvider(): vscode.Disposable[] { 33 | return null; 34 | } 35 | 36 | protected async _createPackage(): Promise { 37 | return; 38 | } 39 | } 40 | 41 | // tslint:disable-next-line: max-classes-per-file 42 | class CatkinMakeBuildTool extends BuildTool { 43 | public static async isApplicable(dir: string): Promise { 44 | return pfs.exists(`${dir}/.catkin_workspace`); 45 | } 46 | 47 | protected _registerTaskProvider(): vscode.Disposable[] { 48 | return [ 49 | vscode.tasks.registerTaskProvider("catkin_make", new catkin.CatkinMakeProvider()), 50 | vscode.tasks.registerTaskProvider("catkin_make_isolated", new catkin.CatkinMakeIsolatedProvider()), 51 | ]; 52 | } 53 | 54 | protected async _createPackage(): Promise { 55 | return catkin.createPackage(); 56 | } 57 | } 58 | 59 | // tslint:disable-next-line: max-classes-per-file 60 | class CatkinToolsBuildTool extends BuildTool { 61 | public static async isApplicable(dir: string): Promise { 62 | return pfs.exists(`${dir}/.catkin_tools`); 63 | } 64 | 65 | protected _registerTaskProvider(): vscode.Disposable[] { 66 | return [vscode.tasks.registerTaskProvider("catkin", new catkin_tools.CatkinToolsProvider())]; 67 | } 68 | 69 | protected async _createPackage(): Promise { 70 | return catkin_tools.createPackage(); 71 | } 72 | } 73 | 74 | // tslint:disable-next-line: max-classes-per-file 75 | class ColconBuildTool extends BuildTool { 76 | public static async isApplicable(dir: string): Promise { 77 | return colcon.isApplicable(dir); 78 | } 79 | 80 | protected _registerTaskProvider(): vscode.Disposable[] { 81 | return [vscode.tasks.registerTaskProvider("colcon", new colcon.ColconProvider())]; 82 | } 83 | 84 | protected async _createPackage(): Promise { 85 | // Do nothing. 86 | return; 87 | } 88 | } 89 | 90 | BuildTool.current = new NotImplementedBuildTool(); 91 | 92 | /** 93 | * Determines build system and workspace path in use by checking for unique 94 | * auto-generated files. 95 | */ 96 | export async function determineBuildTool(dir: string): Promise { 97 | while (dir && path.dirname(dir) !== dir) { 98 | if (await CatkinMakeBuildTool.isApplicable(dir)) { 99 | BuildTool.current = new CatkinMakeBuildTool(); 100 | return true; 101 | } else if (await CatkinToolsBuildTool.isApplicable(dir)) { 102 | BuildTool.current = new CatkinToolsBuildTool(); 103 | return true; 104 | } else if (await ColconBuildTool.isApplicable(dir)) { 105 | BuildTool.current = new ColconBuildTool(); 106 | return true; 107 | } 108 | 109 | dir = path.dirname(dir); 110 | } 111 | return false; 112 | } 113 | 114 | /** 115 | * Check if a task belongs to our extension. 116 | * @param task Task to check 117 | */ 118 | export function isROSBuildTask(task: vscode.Task) { 119 | const types = new Set(["catkin", "catkin_make", "catkin_make_isolated", "colcon"]); 120 | const isRosTask = types.has(task.definition.type); 121 | const isBuildTask = vscode.TaskGroup.Build === task.group; 122 | return isRosTask && isBuildTask; 123 | } 124 | -------------------------------------------------------------------------------- /src/build-tool/catkin-tools.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as extension from "../extension"; 7 | import * as common from "./common"; 8 | import * as rosShell from "./ros-shell"; 9 | 10 | function makeCatkin(name: string, command: string, args: string[], category?: string): vscode.Task { 11 | const task = rosShell.make(name, {type: command, command, args: ['--workspace', vscode.workspace.rootPath, ...args]}, category) 12 | task.problemMatchers = ["$catkin-gcc"]; 13 | 14 | return task; 15 | } 16 | 17 | /** 18 | * Provides catkin tools build and test tasks. 19 | */ 20 | export class CatkinToolsProvider implements vscode.TaskProvider { 21 | public provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult { 22 | const make = makeCatkin('Catkin Tools - Build', 'catkin', [], 'build'); 23 | make.group = vscode.TaskGroup.Build; 24 | 25 | const test = makeCatkin('Catkin Tools - Test', 'catkin', ['--catkin-make-args', 'run_tests'], 'run_tests'); 26 | test.group = vscode.TaskGroup.Test; 27 | 28 | return [make, test]; 29 | } 30 | 31 | public resolveTask(task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult { 32 | return rosShell.resolve(task); 33 | } 34 | } 35 | 36 | /** 37 | * Interacts with the user to run a `catkin create pkg` command. 38 | */ 39 | export async function createPackage(uri?: vscode.Uri) { 40 | const createPkgCommand = (dependencies: string, name: string): string => { 41 | return `catkin create pkg --catkin-deps ${dependencies} -- ${name}`; 42 | }; 43 | return common._createPackage(createPkgCommand); 44 | } 45 | -------------------------------------------------------------------------------- /src/build-tool/catkin.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as extension from "../extension"; 7 | import * as common from "./common"; 8 | import * as rosShell from "./ros-shell"; 9 | 10 | function makeCatkin(name: string, command: string, args: string[], category?: string): vscode.Task { 11 | const task = rosShell.make(name, {type: command, command, args: ['--directory', vscode.workspace.rootPath, '-DCMAKE_BUILD_TYPE=RelWithDebInfo',...args]}, category) 12 | task.problemMatchers = ["$catkin-gcc"]; 13 | 14 | return task; 15 | } 16 | /** 17 | * Provides catkin_make build and test tasks 18 | */ 19 | export class CatkinMakeProvider implements vscode.TaskProvider { 20 | public provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult { 21 | const make = makeCatkin('Catkin Build', 'catkin_make', [], 'build'); 22 | make.group = vscode.TaskGroup.Build; 23 | 24 | const test = makeCatkin('Catkin Test', 'catkin_make', ['run_tests'], 'run_tests'); 25 | test.group = vscode.TaskGroup.Test; 26 | 27 | return [make, test]; 28 | } 29 | 30 | public resolveTask(task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult { 31 | return rosShell.resolve(task); 32 | } 33 | } 34 | /** 35 | * Provides catkin_make_isolated build and test tasks 36 | */ 37 | export class CatkinMakeIsolatedProvider implements vscode.TaskProvider { 38 | public provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult { 39 | const make = makeCatkin('Catkin Build Isolated', 'catkin_make_isolated', [], 'build'); 40 | make.group = vscode.TaskGroup.Build; 41 | 42 | const test = makeCatkin('Catkin Test Isolated', 'catkin_make_isolated', ['--catkin-make-args', 'run_tests'], 'run_tests'); 43 | test.group = vscode.TaskGroup.Test; 44 | 45 | return [make, test]; 46 | } 47 | 48 | public resolveTask(task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult { 49 | return rosShell.resolve(task); 50 | } 51 | } 52 | 53 | /** 54 | * Interacts with the user to run a `catkin_create_pkg` command. 55 | */ 56 | export async function createPackage(uri?: vscode.Uri) { 57 | const createPkgCommand = (dependencies: string, name: string): string => { 58 | return `catkin_create_pkg ${name} ${dependencies}`; 59 | }; 60 | return common._createPackage(createPkgCommand); 61 | } 62 | -------------------------------------------------------------------------------- /src/build-tool/colcon.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as path from "path"; 7 | import * as child_process from "child_process"; 8 | import * as extension from "../extension"; 9 | import * as common from "./common"; 10 | import * as rosShell from "./ros-shell"; 11 | import { env } from "process"; 12 | 13 | function makeColcon(name: string, command: string, verb: string, args: string[], category?: string): vscode.Task { 14 | let installType = '--symlink-install'; 15 | if (process.platform === "win32") { 16 | 17 | // Use Merge Install on Windows to support adminless builds and deployment. 18 | installType = '--merge-install'; 19 | } 20 | 21 | const task = rosShell.make(name, {type: command, command, args: [verb, installType, '--event-handlers', 'console_cohesion+', '--base-paths', vscode.workspace.rootPath, `--cmake-args`, ...args]}, category) 22 | task.problemMatchers = ["$catkin-gcc"]; 23 | 24 | return task; 25 | } 26 | 27 | /** 28 | * Provides colcon build and test tasks. 29 | */ 30 | export class ColconProvider implements vscode.TaskProvider { 31 | public provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult { 32 | const make = makeColcon('Colcon Build Release', 'colcon', 'build', [`-DCMAKE_BUILD_TYPE=RelWithDebInfo`], 'build'); 33 | make.group = vscode.TaskGroup.Build; 34 | 35 | const makeDebug = makeColcon('Colcon Build Debug', 'colcon', 'build', [`-DCMAKE_BUILD_TYPE=Debug`], 'build'); 36 | makeDebug.group = vscode.TaskGroup.Build; 37 | 38 | const test = makeColcon('Colcon Build Test Release', 'colcon', 'test', [`-DCMAKE_BUILD_TYPE=RelWithDebInfo`], 'test'); 39 | test.group = vscode.TaskGroup.Test; 40 | 41 | const testDebug = makeColcon('Colcon Build Test Debug', 'colcon', 'test', [`-DCMAKE_BUILD_TYPE=Debug`], 'test'); 42 | test.group = vscode.TaskGroup.Test; 43 | 44 | return [make, makeDebug, test, testDebug]; 45 | } 46 | 47 | public resolveTask(task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult { 48 | return rosShell.resolve(task); 49 | } 50 | } 51 | 52 | export async function isApplicable(dir: string): Promise { 53 | let colconCommand: string; 54 | const srcDir = path.join(dir, "src") 55 | 56 | if (process.platform === "win32") { 57 | colconCommand = `colcon --log-base nul list --base-paths \"${srcDir}\"`; 58 | } else { 59 | colconCommand = `colcon --log-base /dev/null list --base-paths ${srcDir}`; 60 | } 61 | 62 | const { stdout, stderr } = await child_process.exec(colconCommand, { env: extension.env }); 63 | 64 | // Does this workspace have packages? 65 | for await (const line of stdout) { 66 | // Yes. 67 | return true; 68 | } 69 | 70 | // no. 71 | return false; 72 | } 73 | -------------------------------------------------------------------------------- /src/build-tool/common.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode" 5 | import * as child_process from "child_process"; 6 | 7 | import * as extension from "../extension" 8 | 9 | /** 10 | * Interacts with the user to run a create package command. 11 | */ 12 | export async function _createPackage(createPkgCommand: (dependencies: string, name:string) => string, uri?: vscode.Uri) { 13 | const name = await vscode.window.showInputBox({ 14 | prompt: "Package name", 15 | validateInput: val => val.match(/^\w+$/) ? "" : "Invalid name", 16 | }); 17 | 18 | if (!name) { 19 | return; 20 | } 21 | 22 | const dependencies = await vscode.window.showInputBox({ 23 | prompt: "Dependencies", 24 | validateInput: val => val.match(/^\s*(\w+\s*)*$/) ? "" : "Invalid dependencies", 25 | }); 26 | 27 | if (typeof dependencies === "undefined") { 28 | return; 29 | } 30 | 31 | const cwd = typeof uri !== "undefined" ? uri.fsPath : `${vscode.workspace.rootPath}/src`; 32 | const opts = { cwd, env: extension.env }; 33 | 34 | child_process.exec(createPkgCommand(dependencies, name), opts, (err, stdout, stderr) => { 35 | if (!err) { 36 | vscode.workspace.openTextDocument(`${cwd}/${name}/package.xml`).then(vscode.window.showTextDocument); 37 | } else { 38 | let message = "Could not create package"; 39 | let index = stderr.indexOf("error:"); 40 | 41 | if (index !== -1) { 42 | message += ": " + stderr.substr(index); 43 | } 44 | 45 | vscode.window.showErrorMessage(message); 46 | } 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/build-tool/ros-shell.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as extension from "../extension"; 7 | 8 | export interface RosTaskDefinition extends vscode.TaskDefinition { 9 | command: string; 10 | args?: string[]; 11 | } 12 | 13 | export class RosShellTaskProvider implements vscode.TaskProvider { 14 | public provideTasks(token?: vscode.CancellationToken): vscode.ProviderResult { 15 | return this.defaultRosTasks(); 16 | } 17 | 18 | public defaultRosTasks(): vscode.Task[] { 19 | const rosCore = make('ros core', {type: 'ros', command: 'roscore'}, 'roscore'); 20 | rosCore.isBackground = true; 21 | rosCore.problemMatchers = ['$roscore']; 22 | 23 | const rosLaunch = make('ros launch', {type: 'ros', command: 'roslaunch', args: ['package_name', 'launch_file.launch']}, 'roslaunch'); 24 | rosLaunch.isBackground = true; 25 | rosLaunch.problemMatchers = ['$roslaunch']; 26 | 27 | return [rosCore, rosLaunch]; 28 | } 29 | 30 | public resolveTask(task: vscode.Task, token?: vscode.CancellationToken): vscode.ProviderResult { 31 | return resolve(task); 32 | } 33 | } 34 | 35 | export function registerRosShellTaskProvider(): vscode.Disposable[] { 36 | return [ 37 | vscode.tasks.registerTaskProvider('ros', new RosShellTaskProvider()), 38 | ]; 39 | } 40 | 41 | export function resolve(task: vscode.Task): vscode.Task { 42 | let definition = task.definition as RosTaskDefinition 43 | const resolvedTask = make(definition.command, definition); 44 | 45 | resolvedTask.isBackground = task.isBackground; 46 | resolvedTask.problemMatchers = task.problemMatchers; 47 | return resolvedTask; 48 | } 49 | 50 | export function make(name: string, definition: RosTaskDefinition, category?: string): vscode.Task { 51 | definition.command = definition.command || definition.type; // Command can be missing in build tasks that have type==command 52 | 53 | const args = definition.args || []; 54 | const task = new vscode.Task(definition, vscode.TaskScope.Workspace, name, definition.command); 55 | 56 | task.execution = new vscode.ShellExecution(definition.command, args, { 57 | env: extension.env, 58 | }); 59 | return task; 60 | } 61 | -------------------------------------------------------------------------------- /src/cpp-formatter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as vscode from "vscode"; 6 | 7 | /** 8 | * @link https://github.com/davetcoleman/roscpp_code_format 9 | */ 10 | const CLANG_FORMAT_STYLE = { 11 | BasedOnStyle: "Google", 12 | AccessModifierOffset: -2, 13 | ConstructorInitializerIndentWidth: 2, 14 | AlignEscapedNewlinesLeft: false, 15 | AlignTrailingComments: true, 16 | AllowAllParametersOfDeclarationOnNextLine: false, 17 | AllowShortIfStatementsOnASingleLine: false, 18 | AllowShortLoopsOnASingleLine: false, 19 | AllowShortFunctionsOnASingleLine: "None", 20 | AlwaysBreakTemplateDeclarations: true, 21 | AlwaysBreakBeforeMultilineStrings: true, 22 | BreakBeforeBinaryOperators: false, 23 | BreakBeforeTernaryOperators: false, 24 | BreakConstructorInitializersBeforeComma: true, 25 | BinPackParameters: true, 26 | ColumnLimit: 120, 27 | ConstructorInitializerAllOnOneLineOrOnePerLine: true, 28 | DerivePointerBinding: false, 29 | PointerBindsToType: true, 30 | ExperimentalAutoDetectBinPacking: false, 31 | IndentCaseLabels: true, 32 | MaxEmptyLinesToKeep: 1, 33 | NamespaceIndentation: "None", 34 | ObjCSpaceBeforeProtocolList: true, 35 | PenaltyBreakBeforeFirstCallParameter: 19, 36 | PenaltyBreakComment: 60, 37 | PenaltyBreakString: 1, 38 | PenaltyBreakFirstLessLess: 1000, 39 | PenaltyExcessCharacter: 1000, 40 | PenaltyReturnTypeOnItsOwnLine: 90, 41 | SpacesBeforeTrailingComments: 2, 42 | Cpp11BracedListStyle: false, 43 | Standard: "Auto", 44 | IndentWidth: 2, 45 | TabWidth: 2, 46 | UseTab: "Never", 47 | IndentFunctionDeclarationAfterType: false, 48 | SpacesInParentheses: false, 49 | SpacesInAngles: false, 50 | SpaceInEmptyParentheses: false, 51 | SpacesInCStyleCastParentheses: false, 52 | SpaceAfterControlStatementKeyword: true, 53 | SpaceBeforeAssignmentOperators: true, 54 | ContinuationIndentWidth: 4, 55 | SortIncludes: false, 56 | SpaceAfterCStyleCast: false, 57 | BreakBeforeBraces: "Custom", 58 | BraceWrapping: { 59 | AfterClass: true, 60 | AfterControlStatement: true, 61 | AfterEnum : true, 62 | AfterFunction : true, 63 | AfterNamespace : true, 64 | AfterStruct : true, 65 | AfterUnion : true, 66 | BeforeCatch : true, 67 | BeforeElse : true, 68 | IndentBraces : false 69 | } 70 | }; 71 | 72 | /** 73 | * Formats C++ source using clang-format. 74 | */ 75 | export class CppFormatter implements vscode.DocumentFormattingEditProvider { 76 | public provideDocumentFormattingEdits(document: vscode.TextDocument, 77 | options: vscode.FormattingOptions, 78 | token: vscode.CancellationToken): Thenable { 79 | const tabs = options.insertSpaces ? "Never" : "Always"; 80 | const custom = { TabWidth: options.tabSize, UseTab: tabs }; 81 | const style = JSON.stringify(Object.assign(CLANG_FORMAT_STYLE, custom)); 82 | 83 | return new Promise((resolve, reject) => { 84 | const process = child_process.exec(`clang-format -style='${style}'`, (err, out) => { 85 | if (!err) { 86 | const lastLine = document.lineCount - 1; 87 | const lastChar = document.lineAt(lastLine).text.length; 88 | const range = new vscode.Range(0, 0, lastLine, lastChar); 89 | const edit = vscode.TextEdit.replace(range, out); 90 | 91 | resolve([edit]); 92 | } else { 93 | reject(err); 94 | } 95 | }); 96 | 97 | process.stdin.end(document.getText()); 98 | }); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/debugger/configuration/providers/ros.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from "path"; 5 | import * as vscode from "vscode"; 6 | import { rosApi } from "../../../ros/ros"; 7 | 8 | // interact with the user to create a roslaunch or rosrun configuration 9 | export class RosDebugConfigurationProvider implements vscode.DebugConfigurationProvider { 10 | public async provideDebugConfigurations( 11 | folder: vscode.WorkspaceFolder | undefined, 12 | token?: vscode.CancellationToken): Promise { 13 | const type = await vscode.window.showQuickPick( 14 | ["ROS: Launch", "ROS: Debug Launch File", "ROS: Attach"], { placeHolder: "Choose a request type" }); 15 | if (!type) { 16 | return []; 17 | } 18 | 19 | switch (type) { 20 | case "ROS: Debug Launch File": 21 | case "ROS: Launch": { 22 | const packageName = await vscode.window.showQuickPick(rosApi.getPackageNames(), { 23 | placeHolder: "Choose a package", 24 | }); 25 | if (!packageName) { 26 | return []; 27 | } 28 | const launchFiles = (await rosApi.findPackageLaunchFiles(packageName)).concat(await rosApi.findPackageTestFiles(packageName)); 29 | const launchFileBasenames = launchFiles.map((filename) => path.basename(filename)); 30 | const target = await vscode.window.showQuickPick( 31 | launchFileBasenames, { placeHolder: "Choose a launch file" }); 32 | const launchFilePath = launchFiles[launchFileBasenames.indexOf(target)]; 33 | if (!launchFilePath) { 34 | return []; 35 | } 36 | 37 | if (type === "ROS: Debug Launch File") { 38 | return [{ 39 | name: type, 40 | request: "debug_launch", 41 | target: `${launchFilePath}`, 42 | type: "ros", 43 | }]; 44 | } else { 45 | return [{ 46 | name: type, 47 | request: "launch", 48 | target: `${launchFilePath}`, 49 | launch: ["rviz", "gz", "gzclient", "gzserver"], 50 | type: "ros", 51 | }]; 52 | } 53 | } 54 | case "ROS: Attach": { 55 | return [{ 56 | name: "ROS: Attach", 57 | request: "attach", 58 | type: "ros", 59 | }]; 60 | } 61 | } 62 | 63 | return []; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/debugger/configuration/resolvers/attach.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | import * as child_process from "child_process"; 6 | import * as os from "os"; 7 | import * as port_finder from "portfinder"; 8 | import * as sudo from "sudo-prompt"; 9 | import * as util from "util"; 10 | 11 | import * as extension from "../../../extension"; 12 | 13 | import * as process_picker from "../../process-picker/process-picker"; 14 | import * as picker_items_provider_factory from "../../process-picker/process-items-provider"; 15 | import * as requests from "../../requests"; 16 | import * as utils from "../../utils"; 17 | 18 | const promisifiedExec = util.promisify(child_process.exec); 19 | const promisifiedSudoExec = util.promisify( 20 | (command: any, options: any, cb: any) => 21 | sudo.exec(command, options, 22 | (error, stdout, stderr) => cb(error, stdout))); 23 | 24 | export interface IResolvedAttachRequest extends requests.IAttachRequest { 25 | runtime: string; 26 | processId: number; 27 | commandLine: string; 28 | } 29 | 30 | export class AttachResolver implements vscode.DebugConfigurationProvider { 31 | private readonly supportedRuntimeTypes = [ 32 | "C++", 33 | "Python", 34 | ]; 35 | 36 | public async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: requests.IAttachRequest, token?: vscode.CancellationToken): Promise { 37 | // ${command:} variables only get resolved before passed to debug adapter, need to be manually resolve here 38 | // all ${action:} variables need to be resolved before our resolver propagates the configuration to actual debugger 39 | 40 | await this.resolveRuntimeIfNeeded(this.supportedRuntimeTypes, config); 41 | await this.resolveProcessIdIfNeeded(config); 42 | await this.resolveCommandLineIfNeeded(config); 43 | 44 | // propagate debug configuration to Python or C++ debugger depending on the chosen runtime type 45 | this.launchAttachSession(config as IResolvedAttachRequest); 46 | 47 | // Return null as we have spawned new debug session 48 | return null; 49 | } 50 | 51 | private async launchAttachSession(config: IResolvedAttachRequest) { 52 | if (!config.runtime || !config.processId) { 53 | return; 54 | } 55 | 56 | let debugConfig: ICppvsdbgAttachConfiguration | ICppdbgAttachConfiguration | IPythonAttachConfiguration; 57 | if (config.runtime === "C++") { 58 | if (os.platform() === "win32") { 59 | const cppvsdbgAttachConfig: ICppvsdbgAttachConfiguration = { 60 | name: `C++: ${config.processId}`, 61 | type: "cppvsdbg", 62 | request: "attach", 63 | processId: config.processId, 64 | }; 65 | debugConfig = cppvsdbgAttachConfig; 66 | } else { 67 | const cppdbgAttachConfig: ICppdbgAttachConfiguration = { 68 | name: `C++: ${config.processId}`, 69 | type: "cppdbg", 70 | request: "attach", 71 | program: config.commandLine, 72 | processId: config.processId, 73 | setupCommands: [ 74 | { 75 | text: "-enable-pretty-printing", 76 | description: "Enable pretty-printing for gdb", 77 | ignoreFailures: true 78 | } 79 | ] 80 | }; 81 | debugConfig = cppdbgAttachConfig; 82 | } 83 | 84 | } else if (config.runtime === "Python") { 85 | const host = "localhost"; 86 | const port = await port_finder.getPortPromise(); 87 | const ptvsdInjectCommand = await utils.getPtvsdInjectCommand(host, port, config.processId); 88 | try { 89 | if (os.platform() === "win32") { 90 | const processOptions: child_process.ExecOptions = { 91 | cwd: vscode.workspace.rootPath, 92 | env: await extension.resolvedEnv(), 93 | }; 94 | 95 | // "ptvsd --pid" works with child_process.exec() on Windows 96 | const result = await promisifiedExec(ptvsdInjectCommand, processOptions); 97 | } else { 98 | const processOptions = { 99 | name: "ptvsd", 100 | }; 101 | 102 | // "ptvsd --pid" requires elevated permission on Ubuntu 103 | const result = await promisifiedSudoExec(ptvsdInjectCommand, processOptions); 104 | } 105 | 106 | } catch (error) { 107 | const errorMsg = `Command [${ptvsdInjectCommand}] failed!`; 108 | throw (new Error(errorMsg)); 109 | } 110 | 111 | let statusMsg = `New ptvsd instance running on ${host}:${port} `; 112 | statusMsg += `injected into process [${config.processId}].` + os.EOL; 113 | statusMsg += `To re-attach to process [${config.processId}] after disconnecting, `; 114 | statusMsg += `please create a separate Python remote attach debug configuration `; 115 | statusMsg += `that uses the host and port listed above.`; 116 | extension.outputChannel.appendLine(statusMsg); 117 | extension.outputChannel.show(true); 118 | vscode.window.showInformationMessage(statusMsg); 119 | 120 | const pythonattachdebugconfiguration: IPythonAttachConfiguration = { 121 | name: `Python: ${config.processId}`, 122 | type: "python", 123 | request: "attach", 124 | port: port, 125 | host: host, 126 | }; 127 | debugConfig = pythonattachdebugconfiguration; 128 | } 129 | 130 | if (!debugConfig) { 131 | return; 132 | } 133 | const launched = await vscode.debug.startDebugging(undefined, debugConfig); 134 | if (!launched) { 135 | throw (new Error(`Failed to start debug session!`)); 136 | } 137 | } 138 | 139 | private async resolveRuntimeIfNeeded(supportedRuntimeTypes: string[], config: requests.IAttachRequest) { 140 | if (config.runtime && config.runtime !== "${action:pick}") { 141 | return; 142 | } 143 | 144 | const chooseRuntimeOptions: vscode.QuickPickOptions = { 145 | placeHolder: "Choose runtime type of node to attach to.", 146 | }; 147 | config.runtime = await vscode.window.showQuickPick(supportedRuntimeTypes, chooseRuntimeOptions).then((runtime): string => { 148 | if (!runtime) { 149 | throw new Error("Runtime type not chosen!"); 150 | } 151 | return runtime; 152 | }); 153 | } 154 | 155 | private async resolveProcessIdIfNeeded(config: requests.IAttachRequest) { 156 | if (config.processId && config.processId !== "${action:pick}") { 157 | return; 158 | } 159 | 160 | const processItemsProvider = picker_items_provider_factory.LocalProcessItemsProviderFactory.Get(); 161 | const processPicker = new process_picker.LocalProcessPicker(processItemsProvider); 162 | const process = await processPicker.pick(); 163 | config.processId = process.pid; 164 | } 165 | 166 | private async resolveCommandLineIfNeeded(config: requests.IAttachRequest) { 167 | // this step is only needed on Ubuntu when user has specified PID of C++ executable to attach to 168 | if (os.platform() === "win32" || config.commandLine || config.runtime !== "C++") { 169 | return; 170 | } 171 | 172 | if (!config.processId) { 173 | throw (new Error("No PID specified!")); 174 | } 175 | try { 176 | const result = await promisifiedExec(`ls -l /proc/${config.processId}/exe`); 177 | 178 | // contains a space 179 | const searchTerm = "-> "; 180 | const indexOfFirst = result.stdout.indexOf(searchTerm); 181 | config.commandLine = result.stdout.substring(indexOfFirst + searchTerm.length).trim(); 182 | } catch (error) { 183 | throw (new Error(`Failed to resolve command line for process [${config.processId}]!`)); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/debugger/configuration/resolvers/ros2/debug_launch.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as fs from "fs"; 6 | import * as yaml from "js-yaml"; 7 | import * as os from "os"; 8 | import * as path from "path"; 9 | import * as readline from "readline"; 10 | import * as shell_quote from "shell-quote"; 11 | import * as tmp from "tmp"; 12 | import * as util from "util"; 13 | import * as vscode from "vscode"; 14 | 15 | import * as extension from "../../../../extension"; 16 | import * as requests from "../../../requests"; 17 | import * as utils from "../../../utils"; 18 | import { rosApi } from "../../../../ros/ros"; 19 | 20 | const promisifiedExec = util.promisify(child_process.exec); 21 | 22 | interface ILaunchRequest { 23 | nodeName: string; 24 | executable: string; 25 | arguments: string[]; 26 | cwd: string; 27 | env: { [key: string]: string }; 28 | symbolSearchPath?: string; 29 | additionalSOLibSearchPath?: string; 30 | sourceFileMap?: { [key: string]: string }; 31 | launch?: string[]; // Scripts or executables to just launch without attaching a debugger 32 | attachDebugger?: string[]; // If specified, Scripts or executables to debug; otherwise attaches to everything not ignored 33 | } 34 | 35 | function getExtensionFilePath(extensionFile: string): string { 36 | return path.resolve(extension.extPath, extensionFile); 37 | } 38 | 39 | export class LaunchResolver implements vscode.DebugConfigurationProvider { 40 | // tslint:disable-next-line: max-line-length 41 | public async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: requests.ILaunchRequest, token?: vscode.CancellationToken) { 42 | if (!path.isAbsolute(config.target)) { 43 | throw new Error("Launch request requires an absolute path as target."); 44 | } 45 | else if (path.extname(config.target) !== ".py" && path.extname(config.target) !== ".xml") { 46 | throw new Error("Launch request requires an extension '.py' or '.xml' as target."); 47 | } 48 | 49 | const rosExecOptions: child_process.ExecOptions = { 50 | env: { 51 | ...await extension.resolvedEnv(), 52 | ...config.env, 53 | }, 54 | }; 55 | 56 | extension.outputChannel.appendLine("Executing dumper with the following environment:"); 57 | extension.outputChannel.appendLine(JSON.stringify(rosExecOptions.env, null, 2)); 58 | 59 | let ros2_launch_dumper = getExtensionFilePath(path.join("assets", "scripts", "ros2_launch_dumper.py")); 60 | 61 | let args = [config.target] 62 | if (config.arguments) { 63 | for (let arg of config.arguments) { 64 | args.push(`"${arg}"`); 65 | } 66 | } 67 | 68 | const pythonLaunchConfig: IPythonLaunchConfiguration = { 69 | name: ros2_launch_dumper, 70 | type: "python", 71 | request: "launch", 72 | program: ros2_launch_dumper, 73 | args: args, 74 | env: rosExecOptions.env, 75 | stopOnEntry: false, 76 | justMyCode: false, 77 | }; 78 | 79 | const launched = await vscode.debug.startDebugging(undefined, pythonLaunchConfig); 80 | if (!launched) { 81 | throw (new Error(`Failed to start debug session!`)); 82 | } 83 | 84 | // Return null as we have spawned new debug requests 85 | return null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/debugger/debug-session.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as adapter from "@vscode/debugadapter"; 5 | import * as protocol from "@vscode/debugprotocol"; 6 | 7 | import * as requests from "./requests"; 8 | 9 | export class RosDebugSession extends adapter.DebugSession { 10 | public shutdown() { 11 | super.shutdown(); 12 | } 13 | 14 | protected launchRequest(response: protocol.DebugProtocol.LaunchResponse, request: requests.ILaunchRequest): void { 15 | // launch requests are propagated to runtime-specific debugger extensions, 16 | // this is only a proxy session, self-terminate immediately 17 | this.shutdown(); 18 | } 19 | 20 | protected attachRequest(response: protocol.DebugProtocol.AttachResponse, request: requests.IAttachRequest): void { 21 | // attach requests are propagated to runtime-specific debugger extensions, 22 | // this is only a proxy session, self-terminate immediately 23 | this.shutdown(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/debugger/main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as session from "./debug-session"; 5 | 6 | session.RosDebugSession.run(session.RosDebugSession); 7 | -------------------------------------------------------------------------------- /src/debugger/manager.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as ros_provider from "./configuration/providers/ros"; 7 | import * as attach_resolver from "./configuration/resolvers/attach"; 8 | import * as ros1_launch_resolver from "./configuration/resolvers/ros1/launch"; 9 | import * as ros2_launch_resolver from "./configuration/resolvers/ros2/launch"; 10 | import * as debug_launch_resolver from "./configuration/resolvers/ros2/debug_launch"; 11 | import * as requests from "./requests"; 12 | import * as extension from "../extension"; 13 | 14 | class RosDebugManager implements vscode.DebugConfigurationProvider { 15 | private configProvider: ros_provider.RosDebugConfigurationProvider; 16 | private attachResolver: attach_resolver.AttachResolver; 17 | private ros1LaunchResolver: ros1_launch_resolver.LaunchResolver; 18 | private ros2LaunchResolver: ros2_launch_resolver.LaunchResolver; 19 | private launchDebugResolver: debug_launch_resolver.LaunchResolver; 20 | 21 | constructor() { 22 | this.configProvider = new ros_provider.RosDebugConfigurationProvider(); 23 | this.attachResolver = new attach_resolver.AttachResolver(); 24 | this.launchDebugResolver = new debug_launch_resolver.LaunchResolver(); 25 | this.ros1LaunchResolver = new ros1_launch_resolver.LaunchResolver(); 26 | this.ros2LaunchResolver = new ros2_launch_resolver.LaunchResolver(); 27 | } 28 | 29 | public async provideDebugConfigurations(folder: vscode.WorkspaceFolder | undefined, token?: vscode.CancellationToken): Promise { 30 | return this.configProvider.provideDebugConfigurations(folder, token); 31 | } 32 | 33 | public async resolveDebugConfigurationWithSubstitutedVariables(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, token?: vscode.CancellationToken): Promise { 34 | if (config.request === "attach") { 35 | return this.attachResolver.resolveDebugConfigurationWithSubstitutedVariables(folder, config as requests.IAttachRequest, token); 36 | } else if (config.request === "debug_launch") { 37 | if ((typeof extension.env.ROS_VERSION === "undefined") || (extension.env.ROS_VERSION.trim() == "1")) { 38 | throw new Error("Launch file debugging is not supported on ROS 1."); 39 | } else { 40 | return this.launchDebugResolver.resolveDebugConfigurationWithSubstitutedVariables(folder, config as requests.ILaunchRequest, token); 41 | } 42 | } else if (config.request === "launch") { 43 | if ((typeof extension.env.ROS_VERSION === "undefined") || (extension.env.ROS_VERSION.trim() == "1")) { 44 | return this.ros1LaunchResolver.resolveDebugConfigurationWithSubstitutedVariables(folder, config as requests.ILaunchRequest, token); 45 | } else { 46 | return this.ros2LaunchResolver.resolveDebugConfigurationWithSubstitutedVariables(folder, config as requests.ILaunchRequest, token); 47 | } 48 | } 49 | } 50 | } 51 | 52 | export function registerRosDebugManager(context: vscode.ExtensionContext): void { 53 | var rosProvider = new RosDebugManager(); 54 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("ros", rosProvider, vscode.DebugConfigurationProviderTriggerKind.Initial)); 55 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("ros", rosProvider, vscode.DebugConfigurationProviderTriggerKind.Dynamic)); 56 | } 57 | -------------------------------------------------------------------------------- /src/debugger/process-picker/local-process-items-provider.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as process_item from "./process-entry"; 9 | import * as attach_items_provider from "./process-items-provider"; 10 | 11 | export abstract class LocalProcessQuickPickItemsProvider implements attach_items_provider.IProcessQuickPickItemsProvider { 12 | public getItems(): Promise { 13 | return this.getInternalProcessEntries().then((processEntries) => { 14 | // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) 15 | // We can change to localeCompare if this becomes an issue 16 | processEntries.sort((a, b) => { 17 | if (a.name === undefined) { 18 | if (b.name === undefined) { 19 | return 0; 20 | } 21 | return 1; 22 | } 23 | if (b.name === undefined) { 24 | return -1; 25 | } 26 | const aLower: string = a.name.toLowerCase(); 27 | const bLower: string = b.name.toLowerCase(); 28 | if (aLower === bLower) { 29 | return 0; 30 | } 31 | return aLower < bLower ? -1 : 1; 32 | }); 33 | return processEntries.map((p) => process_item.createProcessQuickPickItem(p)); 34 | }); 35 | } 36 | 37 | protected abstract getInternalProcessEntries(): Promise; 38 | } 39 | -------------------------------------------------------------------------------- /src/debugger/process-picker/process-entry.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as vscode from "vscode"; 9 | 10 | export interface IProcessEntry { 11 | commandLine: string; 12 | pid: string; 13 | } 14 | 15 | export class ProcessEntry implements IProcessEntry { 16 | // constructor(readonly name: string, readonly pid: string, readonly commandLine: string) {} 17 | constructor(public name: string, public pid: string, public commandLine: string) {} 18 | } 19 | 20 | export interface IProcessQuickPickItem extends vscode.QuickPickItem { 21 | pid: string; 22 | } 23 | 24 | export function createProcessQuickPickItem(entry: ProcessEntry): IProcessQuickPickItem 25 | { 26 | return { 27 | label: entry.name, 28 | description: entry.pid, 29 | detail: entry.commandLine, 30 | pid: entry.pid, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/debugger/process-picker/process-items-provider-impl-ps.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as os from "os"; 9 | 10 | import * as process_item from "./process-entry"; 11 | import * as local_provider from "./local-process-items-provider"; 12 | import * as utils from "./utils"; 13 | 14 | export class PsProvider extends local_provider.LocalProcessQuickPickItemsProvider { 15 | // Perf numbers: 16 | // OS X 10.10 17 | // | # of processes | Time (ms) | 18 | // |----------------+-----------| 19 | // | 272 | 52 | 20 | // | 296 | 49 | 21 | // | 384 | 53 | 22 | // | 784 | 116 | 23 | // 24 | // Ubuntu 16.04 25 | // | # of processes | Time (ms) | 26 | // |----------------+-----------| 27 | // | 232 | 26 | 28 | // | 336 | 34 | 29 | // | 736 | 62 | 30 | // | 1039 | 115 | 31 | // | 1239 | 182 | 32 | 33 | // ps outputs as a table. With the option "ww", ps will use as much width as necessary. 34 | // However, that only applies to the right-most column. Here we use a hack of setting 35 | // the column header to 50 a's so that the second column will have at least that many 36 | // characters. 50 was chosen because that's the maximum length of a "label" in the 37 | // QuickPick UI in VSCode. 38 | 39 | protected getInternalProcessEntries(): Promise { 40 | let processCmd: string = ''; 41 | switch (os.platform()) { 42 | case 'darwin': 43 | processCmd = PsProcessParser.psDarwinCommand; 44 | break; 45 | case 'linux': 46 | processCmd = PsProcessParser.psLinuxCommand; 47 | break; 48 | default: 49 | return Promise.reject(new Error(`Operating system "${os.platform()}" not support.`)); 50 | } 51 | return utils.execChildProcess(processCmd, null).then((processes) => { 52 | return PsProcessParser.ParseProcessFromPs(processes); 53 | }); 54 | } 55 | } 56 | 57 | // tslint:disable-next-line: max-classes-per-file 58 | export class PsProcessParser { 59 | private static get secondColumnCharacters(): number { return 50; } 60 | private static get commColumnTitle(): string { return Array(PsProcessParser.secondColumnCharacters).join("a"); } 61 | // the BSD version of ps uses '-c' to have 'comm' only output the executable name and not 62 | // the full path. The Linux version of ps has 'comm' to only display the name of the executable 63 | // Note that comm on Linux systems is truncated to 16 characters: 64 | // https://bugzilla.redhat.com/show_bug.cgi?id=429565 65 | // Since 'args' contains the full path to the executable, even if truncated, searching will work as desired. 66 | public static get psLinuxCommand(): string { 67 | return `ps axww -o pid=,comm=${PsProcessParser.commColumnTitle},args=`; 68 | } 69 | public static get psDarwinCommand(): string { 70 | return `ps axww -o pid=,comm=${PsProcessParser.commColumnTitle},args= -c`; 71 | } 72 | 73 | // Only public for tests. 74 | public static ParseProcessFromPs(processes: string): process_item.ProcessEntry[] { 75 | let lines: string[] = processes.split(os.EOL); 76 | return PsProcessParser.ParseProcessFromPsArray(lines); 77 | } 78 | 79 | public static ParseProcessFromPsArray(processArray: string[]): process_item.ProcessEntry[] { 80 | let processEntries: process_item.ProcessEntry[] = []; 81 | 82 | // lines[0] is the header of the table 83 | for (let i: number = 1; i < processArray.length; i++) { 84 | let line: string = processArray[i]; 85 | if (!line) { 86 | continue; 87 | } 88 | 89 | let processEntry: process_item.ProcessEntry = PsProcessParser.parseLineFromPs(line); 90 | processEntries.push(processEntry); 91 | } 92 | 93 | return processEntries; 94 | } 95 | 96 | private static parseLineFromPs(line: string): process_item.ProcessEntry { 97 | // Explanation of the regex: 98 | // - any leading whitespace 99 | // - PID 100 | // - whitespace 101 | // - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character 102 | // for the whitespace separator 103 | // - whitespace 104 | // - args (might be empty) 105 | const psEntry: RegExp = new RegExp(`^\\s*([0-9]+)\\s+(.{${PsProcessParser.secondColumnCharacters - 1}})\\s+(.*)$`); 106 | const matches: RegExpExecArray = psEntry.exec(line); 107 | if (matches && matches.length === 4) { 108 | const pid: string = matches[1].trim(); 109 | const executable: string = matches[2].trim(); 110 | const cmdline: string = matches[3].trim(); 111 | return new process_item.ProcessEntry(executable, pid, cmdline); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/debugger/process-picker/process-items-provider-impl-wmic.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as os from "os"; 9 | 10 | import * as native_provider from "./local-process-items-provider"; 11 | import * as process_item from "./process-entry"; 12 | import * as utils from "./utils"; 13 | 14 | export class WmicProvider extends native_provider.LocalProcessQuickPickItemsProvider { 15 | // Perf numbers on Win10: 16 | // | # of processes | Time (ms) | 17 | // |----------------+-----------| 18 | // | 309 | 413 | 19 | // | 407 | 463 | 20 | // | 887 | 746 | 21 | // | 1308 | 1132 | 22 | 23 | protected getInternalProcessEntries(): Promise { 24 | const wmicCommand: string = 'wmic process get Name,ProcessId,CommandLine /FORMAT:list'; 25 | return utils.execChildProcess(wmicCommand, null).then((processes) => { 26 | return WmicProcessParser.ParseProcessFromWmic(processes); 27 | }); 28 | } 29 | } 30 | 31 | // tslint:disable-next-line: max-classes-per-file 32 | export class WmicProcessParser { 33 | private static get wmicNameTitle(): string { return 'Name'; } 34 | private static get wmicCommandLineTitle(): string { return 'CommandLine'; } 35 | private static get wmicPidTitle(): string { return 'ProcessId'; } 36 | 37 | // Only public for tests. 38 | public static ParseProcessFromWmic(processes: string): process_item.ProcessEntry[] { 39 | let lines: string[] = processes.split(os.EOL); 40 | let currentProcess: process_item.ProcessEntry = new process_item.ProcessEntry(null, null, null); 41 | let processEntries: process_item.ProcessEntry[] = []; 42 | 43 | for (let i: number = 0; i < lines.length; i++) { 44 | let line: string = lines[i]; 45 | if (!line) { 46 | continue; 47 | } 48 | 49 | WmicProcessParser.parseLineFromWmic(line, currentProcess); 50 | 51 | // Each entry of processes has ProcessId as the last line 52 | if (line.lastIndexOf(WmicProcessParser.wmicPidTitle, 0) === 0) { 53 | processEntries.push(currentProcess); 54 | currentProcess = new process_item.ProcessEntry(null, null, null); 55 | } 56 | } 57 | 58 | return processEntries; 59 | } 60 | 61 | private static parseLineFromWmic(line: string, process: process_item.ProcessEntry): void { 62 | let splitter: number = line.indexOf('='); 63 | if (splitter >= 0) { 64 | let key: string = line.slice(0, line.indexOf('=')).trim(); 65 | let value: string = line.slice(line.indexOf('=') + 1).trim(); 66 | if (key === WmicProcessParser.wmicNameTitle) { 67 | process.name = value; 68 | } else if (key === WmicProcessParser.wmicPidTitle) { 69 | process.pid = value; 70 | } else if (key === WmicProcessParser.wmicCommandLineTitle) { 71 | const extendedLengthPath: string = '\\??\\'; 72 | if (value.lastIndexOf(extendedLengthPath, 0) === 0) { 73 | value = value.slice(extendedLengthPath.length); 74 | } 75 | 76 | process.commandLine = value; 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/debugger/process-picker/process-items-provider.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as os from "os"; 9 | 10 | import * as process_item from "./process-entry"; 11 | import * as ps_provider from "./process-items-provider-impl-ps"; 12 | import * as wmic_provider from "./process-items-provider-impl-wmic"; 13 | 14 | export interface IProcessQuickPickItemsProvider { 15 | getItems(): Promise; 16 | } 17 | 18 | export class LocalProcessItemsProviderFactory { 19 | static Get(): IProcessQuickPickItemsProvider { 20 | if (os.platform() === 'win32') { 21 | return new wmic_provider.WmicProvider(); 22 | } else { 23 | return new ps_provider.PsProvider(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/debugger/process-picker/process-picker.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as quick_pick_items_provider from "./process-items-provider"; 9 | import * as process_quick_pick from "./process-quick-pick"; 10 | import * as process_entry from "./process-entry"; 11 | 12 | export class LocalProcessPicker { 13 | constructor(private quickPickItemsProvider: quick_pick_items_provider.IProcessQuickPickItemsProvider) {} 14 | 15 | public pick(): Promise { 16 | return process_quick_pick.showQuickPick(() => this.quickPickItemsProvider.getItems()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/debugger/process-picker/process-quick-pick.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as path from "path"; 9 | import * as vscode from 'vscode'; 10 | 11 | import * as extension from "../../extension"; 12 | 13 | import * as process_entry from "./process-entry"; 14 | 15 | export function getExtensionFilePath(extensionfile: string): string { 16 | return path.resolve(extension.extPath, extensionfile); 17 | } 18 | 19 | class RefreshButton implements vscode.QuickInputButton { 20 | get iconPath(): { dark: vscode.Uri; light: vscode.Uri } { 21 | const refreshImagePathDark: string = getExtensionFilePath(path.join("assets", "process-picker", "refresh_inverse.svg")); 22 | const refreshImagePathLight: string = getExtensionFilePath(path.join("assets", "process-picker", "refresh.svg")); 23 | 24 | return { 25 | dark: vscode.Uri.file(refreshImagePathDark), 26 | light: vscode.Uri.file(refreshImagePathLight) 27 | }; 28 | } 29 | 30 | get tooltip(): string { 31 | return "Refresh process list"; 32 | } 33 | } 34 | 35 | export async function showQuickPick(getAttachItems: () => Promise): Promise { 36 | return getAttachItems().then(processEntries => { 37 | return new Promise((resolve, reject) => { 38 | let quickPick: vscode.QuickPick = vscode.window.createQuickPick(); 39 | quickPick.title = "Attach to process"; 40 | quickPick.canSelectMany = false; 41 | quickPick.matchOnDescription = true; 42 | quickPick.matchOnDetail = true; 43 | quickPick.placeholder = "Select the process to attach to"; 44 | quickPick.items = processEntries; 45 | quickPick.buttons = [new RefreshButton()]; 46 | 47 | let disposables: vscode.Disposable[] = []; 48 | 49 | quickPick.onDidTriggerButton(button => { 50 | getAttachItems().then(processEntries => quickPick.items = processEntries); 51 | }, undefined, disposables); 52 | 53 | quickPick.onDidAccept(() => { 54 | if (quickPick.selectedItems.length !== 1) { 55 | reject(new Error("Process not selected")); 56 | } 57 | 58 | let selected: process_entry.IProcessEntry = { 59 | commandLine: quickPick.selectedItems[0].detail, 60 | pid: quickPick.selectedItems[0].pid, 61 | } 62 | 63 | disposables.forEach(item => item.dispose()); 64 | quickPick.dispose(); 65 | 66 | resolve(selected); 67 | }, undefined, disposables); 68 | 69 | quickPick.onDidHide(() => { 70 | disposables.forEach(item => item.dispose()); 71 | quickPick.dispose(); 72 | 73 | reject(new Error("Process not selected.")); 74 | }, undefined, disposables); 75 | 76 | quickPick.show(); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/debugger/process-picker/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-cpptools 5 | 6 | /* tslint:disable */ 7 | 8 | import * as child_process from "child_process"; 9 | 10 | export function execChildProcess(process: string, workingDirectory: string): Promise { 11 | return new Promise((resolve, reject) => { 12 | child_process.exec(process, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error: Error, stdout: string, stderr: string) => { 13 | 14 | if (error) { 15 | reject(error); 16 | return; 17 | } 18 | 19 | if (stderr && stderr.length > 0 && !stderr.includes('screen size is bogus')) { 20 | reject(new Error(stderr)); 21 | return; 22 | } 23 | 24 | resolve(stdout); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/debugger/requests.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | import * as debug_protocol from "@vscode/debugprotocol"; 6 | 7 | // tslint:disable-next-line: max-line-length 8 | export interface IAttachRequest extends vscode.DebugConfiguration, debug_protocol.DebugProtocol.AttachRequestArguments { 9 | runtime?: string; 10 | processId?: number | string; 11 | } 12 | 13 | // tslint:disable-next-line: max-line-length 14 | export interface ILaunchRequest extends vscode.DebugConfiguration, debug_protocol.DebugProtocol.LaunchRequestArguments { 15 | target: string; 16 | args: Array; 17 | } 18 | -------------------------------------------------------------------------------- /src/debugger/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | import * as util from "util"; 6 | 7 | import * as extension from "../extension"; 8 | import * as telemetry from "../telemetry-helper"; 9 | 10 | export async function oneTimePromiseFromEvent(eventCall, filter = undefined) : Promise { 11 | return new Promise(resolve => { 12 | let disposable: vscode.Disposable; 13 | disposable = eventCall(event => { 14 | if (filter && !filter(event)) { 15 | return; 16 | } 17 | 18 | disposable.dispose(); 19 | resolve(event); 20 | }); 21 | }); 22 | } 23 | 24 | /** 25 | * Gets stringified settings to pass to the debug server. 26 | */ 27 | export async function getDebugSettings(context: vscode.ExtensionContext) { 28 | const reporter = telemetry.getReporter(); 29 | reporter.sendTelemetryCommand(extension.Commands.GetDebugSettings); 30 | 31 | return JSON.stringify({ env: extension.env }); 32 | } 33 | 34 | export async function getPtvsdInjectCommand(host: string, port: number, pid: number): Promise { 35 | // instead of requiring presence of correctly versioned ptvsd from pip (https://github.com/Microsoft/ptvsd) 36 | // use ptvsd shipped with vscode-python to avoid potential version mismatch 37 | 38 | const pyExtensionId: string = "ms-python.python"; 39 | const pyExtension: vscode.Extension = vscode.extensions.getExtension(pyExtensionId); 40 | if (pyExtension) { 41 | if (!pyExtension.isActive) { 42 | await pyExtension.activate(); 43 | } 44 | 45 | // tslint:disable-next-line:strict-boolean-expressions 46 | if (pyExtension.exports && pyExtension.exports.debug) { 47 | // hack python extension's api to get command for injecting ptvsd 48 | 49 | // pass false for waitForDebugger so the --wait flag won't be added 50 | const waitForDebugger: boolean = false; 51 | const ptvsdCommand = await pyExtension.exports.debug.getRemoteLauncherCommand(host, port, waitForDebugger); 52 | 53 | // prepend python interpreter 54 | ptvsdCommand.unshift("python"); 55 | // append the --pid flag 56 | ptvsdCommand.push("--pid", pid.toString()); 57 | return ptvsdCommand.join(" "); 58 | } else { 59 | throw new Error(`Update extension [${pyExtensionId}] to debug Python projects.`); 60 | } 61 | } 62 | throw new Error("Failed to retrieve ptvsd from Python extension!"); 63 | } 64 | -------------------------------------------------------------------------------- /src/debugger/vscode-python.api.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | // copied from https://github.com/microsoft/vscode-azurefunctions 5 | 6 | interface IPythonExtensionApi { 7 | debug: { 8 | /** 9 | * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. 10 | * Users can append another array of strings of what they want to execute along with relevant arguments to Python. 11 | * E.g `['/Users/..../pythonVSCode/pythonFiles/experimental/ptvsd_launcher.py', '--host', 'localhost', '--port', '57039', '--wait']` 12 | */ 13 | getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches?: boolean): Promise 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/debugger/vscode-python.launch.json.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | interface IPythonLaunchConfiguration { 5 | // properties required by vscode.DebugConfiguration with hard-coded values 6 | type: "python"; 7 | request: "launch"; 8 | name: string; 9 | 10 | // properties defined in vscode-python's schema 11 | 12 | /** 13 | * Name of the module to be debugged 14 | */ 15 | module?: string; 16 | 17 | /** 18 | * Absolute path to the program 19 | */ 20 | program?: string; 21 | 22 | /** 23 | * Path (fully qualified) to python executable. Defaults to the value in settings.json 24 | */ 25 | pythonPath?: string; 26 | 27 | /** 28 | * Command line arguments passed to the program 29 | */ 30 | args?: string[]; 31 | 32 | /** 33 | * Automatically stop after launch 34 | */ 35 | stopOnEntry?: boolean; 36 | 37 | /** 38 | * Show return value of functions when stepping 39 | */ 40 | showReturnValue?: boolean; 41 | 42 | /** 43 | * // Where to launch the debug target: internal console, integrated terminal, or external terminal 44 | */ 45 | console?: "internalConsole" | "integratedTerminal" | "externalTerminal"; 46 | 47 | /** 48 | * Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty) 49 | */ 50 | cwd?: string; 51 | 52 | /** 53 | * Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable 54 | */ 55 | env?: { [key: string]: string; }; 56 | 57 | /** 58 | * Absolute path to a file containing environment variable definitions 59 | */ 60 | envFile?: string; 61 | 62 | /** 63 | * Debug port (default is 0, resulting in the use of a dynamic port) 64 | */ 65 | port?: number; 66 | 67 | /** 68 | * IP address of the of the local debug server (default is localhost) 69 | */ 70 | host?: string; 71 | 72 | /** 73 | * Enable logging of debugger events to a log file 74 | */ 75 | logToFile?: boolean; 76 | 77 | /** 78 | * Redirect output 79 | */ 80 | redirectOutput?: boolean; 81 | 82 | /** 83 | * Debug only user-written code 84 | */ 85 | justMyCode?: boolean; 86 | 87 | /** 88 | * Enable debugging of gevent monkey-patched code 89 | */ 90 | gevent?: boolean; 91 | 92 | /** 93 | * Django debugging 94 | */ 95 | django?: boolean; 96 | 97 | /** 98 | * Jinja template debugging (e.g. Flask) 99 | */ 100 | jinja?: true | false | null; 101 | 102 | /** 103 | * Running debug program under elevated permissions (on Unix) 104 | */ 105 | sudo?: boolean; 106 | 107 | /** 108 | * Whether debugging Pyramid applications 109 | */ 110 | pyramid?: boolean; 111 | 112 | /** 113 | * Whether to enable Sub Process debugging 114 | */ 115 | subProcess?: boolean; 116 | } 117 | 118 | interface IPythonAttachConfiguration { 119 | // properties required by vscode.DebugConfiguration with hard-coded values 120 | type: "python"; 121 | request: "attach"; 122 | name: string; 123 | 124 | // properties defined in vscode-python's schema 125 | 126 | /** 127 | * Debug port to attach 128 | */ 129 | port: number; 130 | 131 | /** 132 | * IP Address of the of remote server (default is localhost or use 127.0.0.1) 133 | */ 134 | host?: string; 135 | 136 | /** 137 | * Path mappings 138 | */ 139 | pathMappings?: Array<{ localRoot: string; remoteRoot: string; }>; 140 | 141 | /** 142 | * Enable logging of debugger events to a log file 143 | */ 144 | logToFile?: boolean; 145 | 146 | /** 147 | * Redirect output 148 | */ 149 | redirectOutput?: boolean; 150 | 151 | /** 152 | * Debug only user-written code 153 | */ 154 | justMyCode?: boolean; 155 | 156 | /** 157 | * Django debugging 158 | */ 159 | django?: boolean; 160 | 161 | /** 162 | * Jinja template debugging (e.g. Flask) 163 | */ 164 | jinja?: true | false | null; 165 | 166 | /** 167 | * Whether to enable Sub Process debugging 168 | */ 169 | subProcess?: boolean; 170 | 171 | /** 172 | * Show return value of functions when stepping 173 | */ 174 | showReturnValue?: boolean; 175 | } 176 | -------------------------------------------------------------------------------- /src/promise-fs.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as fs from "fs"; 5 | 6 | function call(fn: Function, ...args: any[]): Promise { 7 | return new Promise((c, e) => fn(...args, (err, res) => err ? e(err) : c(res))); 8 | } 9 | 10 | export function exists(path: string): Promise { 11 | return new Promise(c => fs.exists(path, c)); 12 | } 13 | 14 | export function readFile(filename: string, encoding: string): Promise; 15 | export function readFile(filename: string): Promise; 16 | 17 | export function readFile(filename: string, encoding?: string) { 18 | return call(fs.readFile, ...arguments); 19 | } 20 | 21 | export function readdir(path: string): Promise { 22 | return call(fs.readdir, path); 23 | } 24 | 25 | export function mkdir(path: string): Promise { 26 | return call(fs.mkdir, path); 27 | } 28 | 29 | export function writeFile(filename: string, data: any): Promise { 30 | return call(fs.writeFile, filename, data); 31 | } 32 | -------------------------------------------------------------------------------- /src/ros/build-env-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from "path"; 5 | import * as vscode from "vscode"; 6 | 7 | import * as extension from "../extension"; 8 | import * as pfs from "../promise-fs"; 9 | import * as telemetry from "../telemetry-helper"; 10 | import { rosApi } from "./ros"; 11 | 12 | const PYTHON_AUTOCOMPLETE_PATHS = "python.autoComplete.extraPaths"; 13 | const PYTHON_ANALYSIS_PATHS = "python.analysis.extraPaths"; 14 | 15 | /** 16 | * Creates config files which don't exist. 17 | */ 18 | export async function createConfigFiles() { 19 | const config = vscode.workspace.getConfiguration(); 20 | 21 | // Update the Python autocomplete paths if required. 22 | if (config.get(PYTHON_AUTOCOMPLETE_PATHS, []).length === 0) { 23 | updatePythonAutoCompletePathInternal(); 24 | } 25 | 26 | // Update the Python analysis paths if required. 27 | if (config.get(PYTHON_ANALYSIS_PATHS, []).length === 0) { 28 | updatePythonAnalysisPathInternal(); 29 | } 30 | 31 | const dir = path.join(vscode.workspace.rootPath, ".vscode"); 32 | 33 | // Update the C++ path. 34 | pfs.exists(path.join(dir, "c_cpp_properties.json")).then(exists => { 35 | if (!exists) { 36 | updateCppPropertiesInternal(); 37 | } 38 | }); 39 | } 40 | 41 | export async function updateCppProperties(context: vscode.ExtensionContext): Promise { 42 | const reporter = telemetry.getReporter(); 43 | reporter.sendTelemetryCommand(extension.Commands.UpdateCppProperties); 44 | 45 | updateCppPropertiesInternal(); 46 | } 47 | 48 | /** 49 | * Updates the `c_cpp_properties.json` file with ROS include paths. 50 | */ 51 | async function updateCppPropertiesInternal(): Promise { 52 | let includes = await rosApi.getIncludeDirs(); 53 | const workspaceIncludes = await rosApi.getWorkspaceIncludeDirs(vscode.workspace.rootPath); 54 | includes = includes.concat(workspaceIncludes); 55 | 56 | if (process.platform === "linux") { 57 | includes.push(path.join("/", "usr", "include")); 58 | } 59 | 60 | // append ** so the IntelliSense engine will do a recursive search for hearder files starting from that directory 61 | includes = includes.map((include: string) => { 62 | return path.join(include, "**"); 63 | }); 64 | 65 | // https://github.com/Microsoft/vscode-cpptools/blob/master/Documentation/LanguageServer/c_cpp_properties.json.md 66 | const cppProperties: any = { 67 | configurations: [ 68 | { 69 | browse: { 70 | databaseFilename: "${default}", 71 | limitSymbolsToIncludedHeaders: false, 72 | }, 73 | includePath: includes, 74 | name: "ROS", 75 | }, 76 | ], 77 | version: 4, 78 | }; 79 | 80 | const filename = path.join(vscode.workspace.rootPath, ".vscode", "c_cpp_properties.json"); 81 | 82 | if (process.platform === "linux") { 83 | // set the default configurations. 84 | cppProperties.configurations[0].intelliSenseMode = "gcc-" + process.arch 85 | cppProperties.configurations[0].compilerPath = "/usr/bin/gcc" 86 | cppProperties.configurations[0].cStandard = "gnu11" 87 | cppProperties.configurations[0].cppStandard = getCppStandard() 88 | 89 | // read the existing file 90 | try { 91 | let existing: any = JSON.parse(await pfs 92 | .readFile(filename) 93 | .then(buffer => buffer.toString())); 94 | 95 | // if the existing configurations are different from the defaults, use the existing values 96 | if (existing.configurations && existing.configurations.length > 0) { 97 | const existingConfig = existing.configurations[0]; 98 | 99 | cppProperties.configurations[0].intelliSenseMode = existingConfig.intelliSenseMode || cppProperties.configurations[0].intelliSenseMode; 100 | cppProperties.configurations[0].compilerPath = existingConfig.compilerPath || cppProperties.configurations[0].compilerPath; 101 | cppProperties.configurations[0].cStandard = existingConfig.cStandard || cppProperties.configurations[0].cStandard; 102 | cppProperties.configurations[0].cppStandard = existingConfig.cppStandard || cppProperties.configurations[0].cppStandard; 103 | } 104 | } 105 | catch (error) { 106 | // ignore 107 | } 108 | } 109 | 110 | // Ensure the ".vscode" directory exists then update the C++ path. 111 | const dir = path.join(vscode.workspace.rootPath, ".vscode"); 112 | 113 | if (!await pfs.exists(dir)) { 114 | await pfs.mkdir(dir); 115 | } 116 | 117 | await pfs.writeFile(filename, JSON.stringify(cppProperties, undefined, 2)); 118 | } 119 | 120 | export function updatePythonPath(context: vscode.ExtensionContext) { 121 | const reporter = telemetry.getReporter(); 122 | reporter.sendTelemetryCommand(extension.Commands.UpdatePythonPath); 123 | 124 | updatePythonAutoCompletePathInternal(); 125 | updatePythonAnalysisPathInternal(); 126 | } 127 | 128 | /** 129 | * Updates the python autocomplete path to support ROS. 130 | */ 131 | function updatePythonAutoCompletePathInternal() { 132 | vscode.workspace.getConfiguration().update(PYTHON_AUTOCOMPLETE_PATHS, extension.env.PYTHONPATH.split(path.delimiter)); 133 | } 134 | 135 | /** 136 | * Updates the python analysis path to support ROS. 137 | */ 138 | function updatePythonAnalysisPathInternal() { 139 | vscode.workspace.getConfiguration().update(PYTHON_ANALYSIS_PATHS, extension.env.PYTHONPATH.split(path.delimiter)); 140 | } 141 | 142 | function getCppStandard() { 143 | switch (vscode.workspace.getConfiguration().get("ros.distro")) 144 | { 145 | case "kinetic": 146 | case "lunar": 147 | return "c++11" 148 | case "melodic": 149 | case "noetic": 150 | case "ardent": 151 | case "bouncy": 152 | case "crystal": 153 | case "dashing": 154 | case "eloquent": 155 | case "foxy": 156 | return "c++14" 157 | case "galactic": 158 | case "humble": 159 | case "iron": 160 | case "jazzy": 161 | case "rolling": 162 | return "c++17" 163 | default: 164 | return "c++17" 165 | } 166 | } -------------------------------------------------------------------------------- /src/ros/cli.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from "path"; 5 | import * as vscode from "vscode"; 6 | 7 | import * as extension from "../extension"; 8 | import * as telemetry from "../telemetry-helper"; 9 | import { rosApi } from "./ros" 10 | 11 | export async function rosrun(context: vscode.ExtensionContext) { 12 | const reporter = telemetry.getReporter(); 13 | reporter.sendTelemetryCommand(extension.Commands.Rosrun); 14 | 15 | const terminal = await preparerosrun(); 16 | if (!terminal) { 17 | return; 18 | } 19 | terminal.show(); 20 | } 21 | 22 | async function preparerosrun(): Promise { 23 | const packageName = await vscode.window.showQuickPick(rosApi.getPackageNames(), { 24 | placeHolder: "Choose a package", 25 | }); 26 | if (!packageName) { 27 | return; 28 | } 29 | let basenames = (files: string[]) => files.map((file) => path.basename(file)); 30 | const executables = rosApi.findPackageExecutables(packageName).then(basenames); 31 | let target = await vscode.window.showQuickPick(executables, { placeHolder: "Choose an executable" }); 32 | if (!target) { 33 | return; 34 | } 35 | let argument = await vscode.window.showInputBox({ placeHolder: "Enter any extra arguments" }); 36 | if (argument == undefined) { 37 | return; 38 | } 39 | return rosApi.activateRosrun(packageName, target, argument); 40 | } 41 | 42 | export async function roslaunch(context: vscode.ExtensionContext) { 43 | const reporter = telemetry.getReporter(); 44 | reporter.sendTelemetryCommand(extension.Commands.Roslaunch); 45 | 46 | let terminal = await prepareroslaunch(); 47 | if (!terminal) { 48 | return; 49 | } 50 | terminal.show(); 51 | } 52 | 53 | async function prepareroslaunch(): Promise { 54 | const packageName = await vscode.window.showQuickPick(rosApi.getPackageNames(), { 55 | placeHolder: "Choose a package", 56 | }); 57 | if (!packageName) { 58 | return; 59 | } 60 | const launchFiles = await rosApi.findPackageLaunchFiles(packageName); 61 | const launchFileBasenames = launchFiles.map((filename) => path.basename(filename)); 62 | let target = await vscode.window.showQuickPick(launchFileBasenames, { placeHolder: "Choose a launch file" }); 63 | const launchFilePath = launchFiles[launchFileBasenames.indexOf(target)]; 64 | if (!launchFilePath) { 65 | return; 66 | } 67 | let argument = await vscode.window.showInputBox({ placeHolder: "Enter any extra arguments" }); 68 | if (argument == undefined) { 69 | return; 70 | } 71 | return rosApi.activateRoslaunch(launchFilePath, argument); 72 | } 73 | 74 | export async function rostest(context: vscode.ExtensionContext) { 75 | const reporter = telemetry.getReporter(); 76 | reporter.sendTelemetryCommand(extension.Commands.Rostest); 77 | 78 | let terminal = await preparerostest(); 79 | if (!terminal) { 80 | return; 81 | } 82 | terminal.show(); 83 | } 84 | 85 | async function preparerostest(): Promise { 86 | const packageName = await vscode.window.showQuickPick(rosApi.getPackageNames(), { 87 | placeHolder: "Choose a package", 88 | }); 89 | if (!packageName) { 90 | return; 91 | } 92 | const launchFiles = (await rosApi.findPackageLaunchFiles(packageName)).concat(await rosApi.findPackageTestFiles(packageName)); 93 | const launchFileBasenames = launchFiles.map((filename) => path.basename(filename)); 94 | let target = await vscode.window.showQuickPick(launchFileBasenames, { placeHolder: "Choose a launch file" }); 95 | const launchFilePath = launchFiles[launchFileBasenames.indexOf(target)]; 96 | if (!launchFilePath) { 97 | return; 98 | } 99 | let argument = await vscode.window.showInputBox({ placeHolder: "Enter any extra arguments" }); 100 | if (argument == undefined) { 101 | return; 102 | } 103 | return rosApi.activateRostest(launchFilePath, argument); 104 | } 105 | -------------------------------------------------------------------------------- /src/ros/common/unknown-ros.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as ros from "../ros"; 7 | 8 | /** 9 | * Provides behavior for unknown ROS environment. 10 | */ 11 | export class UnknownROS implements ros.ROSApi { 12 | public setContext(context: vscode.ExtensionContext, env: any) { 13 | } 14 | 15 | public getPackageNames(): Promise { 16 | console.error("Unknown ROS distro."); 17 | return; 18 | } 19 | 20 | public getPackages(): Promise<{ [name: string]: () => Promise }> { 21 | console.error("Unknown ROS distro."); 22 | return; 23 | } 24 | 25 | public getIncludeDirs(): Promise { 26 | console.error("Unknown ROS distro."); 27 | return; 28 | } 29 | 30 | public getWorkspaceIncludeDirs(workspaceDir: string): Promise { 31 | console.error("Unknown ROS distro."); 32 | return; 33 | } 34 | 35 | public findPackageExecutables(packageName: string): Promise { 36 | console.error("Unknown ROS distro."); 37 | return; 38 | } 39 | 40 | public findPackageLaunchFiles(packageName: string): Promise { 41 | console.error("Unknown ROS distro."); 42 | return; 43 | } 44 | 45 | public findPackageTestFiles(packageName: string): Promise { 46 | console.error("Unknown ROS distro."); 47 | return; 48 | } 49 | 50 | public startCore() { 51 | console.error("Unknown ROS distro."); 52 | return; 53 | } 54 | 55 | public stopCore() { 56 | console.error("Unknown ROS distro."); 57 | return; 58 | } 59 | 60 | public getCoreStatus(): Promise { 61 | console.error("Unknown ROS distro."); 62 | return; 63 | } 64 | 65 | public rosdep() { 66 | console.error("Unknown ROS distro."); 67 | return; 68 | } 69 | 70 | public activateCoreMonitor(): vscode.Disposable { 71 | console.error("Unknown ROS distro."); 72 | return; 73 | } 74 | 75 | public showCoreMonitor() { 76 | console.error("Unknown ROS distro."); 77 | return; 78 | } 79 | 80 | public activateRosrun(packageName: string, executableName:string, argument: string): vscode.Terminal { 81 | console.error("Unknown ROS distro."); 82 | return; 83 | } 84 | 85 | public activateRoslaunch(launchFilepath: string, argument: string): vscode.Terminal { 86 | console.error("Unknown ROS distro."); 87 | return; 88 | } 89 | 90 | public activateRostest(launchFilepath: string, argument: string): vscode.Terminal { 91 | console.error("Unknown ROS distro."); 92 | return; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ros/ros.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | import * as unknownROS from "./common/unknown-ros"; 7 | import * as ros1 from "./ros1/ros1"; 8 | import * as ros2 from "./ros2/ros2"; 9 | 10 | export interface ROSApi { 11 | /** 12 | * Set additional context. 13 | */ 14 | setContext: (context: vscode.ExtensionContext, env: any) => void; 15 | 16 | /** 17 | * Get a list of packages. 18 | */ 19 | getPackageNames: () => Promise; 20 | 21 | /** 22 | * Gets a map of package names to paths. 23 | */ 24 | getPackages: () => Promise<{ [name: string]: () => Promise }>; 25 | 26 | /** 27 | * Gets a list of CMake include paths. 28 | */ 29 | getIncludeDirs: () => Promise; 30 | 31 | /** 32 | * Gets a list of include paths under current workspace. 33 | * @param workspaceDir 34 | */ 35 | getWorkspaceIncludeDirs: (workspaceDir: string) => Promise; 36 | 37 | /** 38 | * list full paths to all executables inside a package 39 | * @param packageName 40 | */ 41 | findPackageExecutables: (packageName: string) => Promise; 42 | 43 | /** 44 | * list all .launch files inside a package 45 | * @param packageName 46 | */ 47 | findPackageLaunchFiles: (packageName: string) => Promise; 48 | 49 | /** 50 | * list all .test files inside a package 51 | * @param packageName 52 | */ 53 | findPackageTestFiles: (packageName: string) => Promise; 54 | 55 | /** 56 | * Start ROS Core 57 | */ 58 | startCore: () => void; 59 | 60 | /** 61 | * Stop ROS Core 62 | */ 63 | stopCore: () => void; 64 | 65 | /** 66 | * Get ROS Core status 67 | */ 68 | getCoreStatus: () => Promise; 69 | 70 | /** 71 | * Run ROSDep 72 | */ 73 | rosdep: () => void; 74 | 75 | /** 76 | * Activate ROS Core monitor. 77 | */ 78 | activateCoreMonitor: () => vscode.Disposable; 79 | 80 | /** 81 | * Bring up ROS Core monitor GUI. 82 | */ 83 | showCoreMonitor: () => void; 84 | 85 | /** 86 | * Activate a terminal for rosrun. 87 | */ 88 | activateRosrun: (packageName: string, executableName:string, argument: string) => vscode.Terminal; 89 | 90 | /** 91 | * Activate a terminal for roslaunch. 92 | */ 93 | activateRoslaunch: (launchFilepath: string, argument: string) => vscode.Terminal; 94 | 95 | /** 96 | * Activate a terminal for rostest. 97 | */ 98 | activateRostest: (launchFilepath: string, argument: string) => vscode.Terminal; 99 | } 100 | 101 | const ros1Api: ROSApi = new ros1.ROS1(); 102 | const ros2Api: ROSApi = new ros2.ROS2(); 103 | const unknownRosApi: ROSApi = new unknownROS.UnknownROS(); 104 | 105 | export let rosApi: ROSApi = unknownRosApi; 106 | 107 | export function selectROSApi(version: string) { 108 | rosApi = unknownRosApi; 109 | switch(version.trim()) { 110 | case "1": { 111 | rosApi = ros1Api; 112 | break; 113 | } 114 | case "2": { 115 | rosApi = ros2Api; 116 | break; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/ros/ros1/core-helper.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as path from "path"; 6 | import * as vscode from "vscode"; 7 | import * as xmlrpc from "xmlrpc"; 8 | 9 | import * as extension from "../../extension"; 10 | import * as telemetry from "../../telemetry-helper"; 11 | 12 | export function startCore(context: vscode.ExtensionContext) { 13 | const reporter = telemetry.getReporter(); 14 | reporter.sendTelemetryCommand(extension.Commands.StartRosCore); 15 | 16 | let launchCoreCommand: string = "roscore"; 17 | let processOptions: child_process.SpawnOptions = { 18 | cwd: vscode.workspace.rootPath, 19 | env: extension.env, 20 | }; 21 | 22 | const roscoreProcess = child_process.spawn(launchCoreCommand, processOptions); 23 | roscoreProcess.on('error', (_err) => { 24 | vscode.window.showErrorMessage("Failed to launch ROS core."); 25 | }); 26 | } 27 | 28 | export function stopCore(context: vscode.ExtensionContext, api: XmlRpcApi) { 29 | const reporter = telemetry.getReporter(); 30 | reporter.sendTelemetryCommand(extension.Commands.TerminateRosCore); 31 | 32 | if (process.platform === "win32") { 33 | api.getPid().then(pid => child_process.exec(`taskkill /pid ${pid} /f`)); 34 | } 35 | else { 36 | api.getPid().then(pid => child_process.exec(`kill $(ps -o ppid= -p '${pid}')`)); 37 | } 38 | } 39 | 40 | export function launchMonitor(context: vscode.ExtensionContext) { 41 | const reporter = telemetry.getReporter(); 42 | reporter.sendTelemetryCommand(extension.Commands.ShowCoreStatus); 43 | 44 | const panel = vscode.window.createWebviewPanel( 45 | "rosCoreStatus", 46 | "ROS Core Status", 47 | vscode.ViewColumn.Two, 48 | { 49 | enableScripts: true, 50 | } 51 | ); 52 | 53 | let stylesheet = panel.webview.asWebviewUri(vscode.Uri.file(path.join(context.extensionPath, "assets", "ros", "core-monitor", "style.css"))); 54 | let script = panel.webview.asWebviewUri(vscode.Uri.file(path.join(context.extensionPath, "dist", "ros1_webview_main.js"))); 55 | 56 | panel.webview.html = getCoreStatusWebviewContent(stylesheet, script); 57 | 58 | const pollingStatus = setInterval(() => { 59 | const masterApi = new XmlRpcApi(extension.env.ROS_MASTER_URI); 60 | masterApi.check().then((status: boolean) => { 61 | if (status) { 62 | let getParameters = masterApi.getParam("/"); 63 | let getSystemState = masterApi.getSystemState(); 64 | 65 | Promise.all([getParameters, getSystemState]).then(([parameters, systemState]) => { 66 | let parametersJSON = JSON.stringify(parameters); 67 | let systemStateJSON = JSON.stringify(systemState); 68 | 69 | panel.webview.postMessage({ 70 | status: status, 71 | parameters: parametersJSON, 72 | systemState: systemStateJSON, 73 | }); 74 | }); 75 | } 76 | else { 77 | panel.webview.postMessage({ 78 | status: status, 79 | }); 80 | } 81 | }); 82 | }, 100); 83 | panel.onDidDispose(() => { 84 | clearInterval(pollingStatus); 85 | }); 86 | } 87 | 88 | function getCoreStatusWebviewContent(stylesheet: vscode.Uri, script: vscode.Uri): string { 89 | return ` 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |

ROS Core Status

101 |

-

102 | 103 |
104 |
105 |
106 | 107 | 108 | 109 | `; 110 | } 111 | 112 | interface ISystemState { 113 | publishers: { [topic: string]: string[] }; 114 | subscribers: { [topic: string]: string[] }; 115 | services: { [service: string]: string[] }; 116 | } 117 | 118 | const CALLER_ID = "vscode-ros"; 119 | 120 | /** 121 | * Exposes the ROS master XML-RPC api. 122 | */ 123 | export class XmlRpcApi { 124 | private client: xmlrpc.Client; 125 | 126 | public constructor(uri: string) { 127 | this.client = xmlrpc.createClient(uri); 128 | } 129 | 130 | /** 131 | * Returns true if a master process is running. 132 | */ 133 | public check(): Promise { 134 | return this.getPid().then(() => true, () => false); 135 | } 136 | 137 | public getPid(): Promise { 138 | return this.methodCall("getPid"); 139 | } 140 | 141 | public getSystemState(): Promise { 142 | const responseReducer = (acc: object, cur: any[]) => { 143 | const k: string = cur[0] as string; 144 | const v: string[] = cur[1] as string[]; 145 | acc[k] = v; 146 | return acc; 147 | }; 148 | return this.methodCall("getSystemState").then((res) => { 149 | const systemState: ISystemState = { 150 | publishers: res[0].reduce(responseReducer, {}), 151 | services: res[2].reduce(responseReducer, {}), 152 | subscribers: res[1].reduce(responseReducer, {}), 153 | }; 154 | return systemState; 155 | }); 156 | } 157 | 158 | public getParamNames(): Promise { 159 | return this.methodCall("getParamNames"); 160 | } 161 | 162 | public getParam(name: string): Promise { 163 | return this.methodCall("getParam", name); 164 | } 165 | 166 | private methodCall(method: string, ...args: any[]): Promise { 167 | return new Promise((resolve, reject) => { 168 | this.client.methodCall(method, [CALLER_ID, ...args], (err, val) => { 169 | if (err) { 170 | reject(err); 171 | } else if (val[0] !== 1) { 172 | reject(val); 173 | } else { 174 | resolve(val[2]); 175 | } 176 | }); 177 | }); 178 | } 179 | } 180 | 181 | /** 182 | * Shows the ROS core status in the status bar. 183 | */ 184 | // tslint:disable-next-line: max-classes-per-file 185 | export class StatusBarItem { 186 | private item: vscode.StatusBarItem; 187 | private timer: NodeJS.Timer; 188 | private status: boolean; 189 | 190 | public constructor(private api: XmlRpcApi) { 191 | this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 200); 192 | 193 | const waitIcon = "$(clock)"; 194 | const ros = "ROS"; 195 | this.item.text = `${waitIcon} ${ros}`; 196 | this.item.command = extension.Commands.ShowCoreStatus; 197 | } 198 | 199 | public activate() { 200 | this.item.show(); 201 | this.timer = setInterval(() => this.update(), 200); 202 | } 203 | 204 | public dispose() { 205 | this.item.dispose(); 206 | } 207 | 208 | private async update() { 209 | const status = await this.api.check(); 210 | 211 | if (status === this.status) { 212 | return; 213 | } 214 | 215 | const statusIcon = status ? "$(check)" : "$(x)"; 216 | let ros = "ROS"; 217 | 218 | // these environment variables are set by the ros_environment package 219 | // https://github.com/ros/ros_environment 220 | const rosVersionChecker = "ROS_VERSION"; 221 | const rosDistroChecker = "ROS_DISTRO"; 222 | if (rosVersionChecker in extension.env && rosDistroChecker in extension.env) { 223 | const rosVersion: string = extension.env[rosVersionChecker]; 224 | const rosDistro: string = extension.env[rosDistroChecker]; 225 | ros += `${rosVersion}.${rosDistro}`; 226 | } else { 227 | // for older ROS1 installations with outdated ros_environment 228 | // "rosversion --distro" might be needed 229 | // ignoring such case for now 230 | } 231 | this.item.text = `${statusIcon} ${ros}`; 232 | this.status = status; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/ros/ros1/ros1.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import * as path from "path"; 8 | import * as util from "util"; 9 | import * as vscode from "vscode"; 10 | 11 | import * as ros from "../ros"; 12 | import * as ros_core from "./core-helper"; 13 | import * as ros_utils from "../utils"; 14 | 15 | const promisifiedExists = util.promisify(fs.exists); 16 | 17 | export class ROS1 implements ros.ROSApi { 18 | private context: vscode.ExtensionContext; 19 | private env: any; 20 | private xmlRpcApi: ros_core.XmlRpcApi = null; 21 | 22 | public setContext(context: vscode.ExtensionContext, env: any) { 23 | this.context = context; 24 | this.env = env; 25 | } 26 | 27 | public getPackageNames(): Promise { 28 | return this.getPackages().then((packages: { [name: string]: () => Promise }) => { 29 | return Object.keys(packages); 30 | }); 31 | } 32 | 33 | public getPackages(): Promise<{ [name: string]: () => Promise }> { 34 | return new Promise((resolve, reject) => child_process.exec("rospack list", { env: this.env }, (err, out) => { 35 | if (!err) { 36 | const lines = out.trim().split(os.EOL).map(((line) => { 37 | const info: string[] = line.split(" "); 38 | if (info.length === 2) { 39 | // each line should contain exactly 2 strings separated by 1 space 40 | return info; 41 | } 42 | })); 43 | 44 | const packageInfoReducer = (acc: object, cur: string[]) => { 45 | const k: string = cur[0] as string; 46 | const v: string = cur[1] as string; 47 | acc[k] = async () => { 48 | return v; 49 | }; 50 | return acc; 51 | }; 52 | resolve(lines.reduce(packageInfoReducer, {})); 53 | } else { 54 | reject(err); 55 | } 56 | })); 57 | } 58 | 59 | public getIncludeDirs(): Promise { 60 | const cmakePrefixPaths: string[] = []; 61 | if (this.env.hasOwnProperty("CMAKE_PREFIX_PATH")) { 62 | cmakePrefixPaths.push(...this.env.CMAKE_PREFIX_PATH.split(path.delimiter)); 63 | } 64 | 65 | const includeDirs: string[] = []; 66 | const fsPromises = cmakePrefixPaths.map((dir: string) => { 67 | const include = path.join(dir, "include"); 68 | return fs.promises.access(include, fs.constants.F_OK) 69 | .then(() => { 70 | includeDirs.push(include); 71 | }) 72 | .catch(() => { 73 | // suppress exception if include folder does not exist 74 | }); 75 | }); 76 | return Promise.all(fsPromises).then(() => { 77 | return includeDirs; 78 | }); 79 | } 80 | 81 | public async getWorkspaceIncludeDirs(workspaceDir: string): Promise { 82 | // Get all packages within the workspace that have an include directory 83 | const packages = await this.getPackages(); 84 | const filteredPackages = await Object.values(packages).filter(async (packagePath: () => Promise) => { 85 | const packageBasePath = await packagePath(); 86 | return packageBasePath.startsWith(workspaceDir); 87 | }); 88 | 89 | const includes: string[] = []; 90 | for (const pkg of filteredPackages) { 91 | const packageBasePath = await pkg(); 92 | const include = path.join(packageBasePath, "include"); 93 | 94 | if (await promisifiedExists(include)) { 95 | includes.push(include); 96 | } 97 | } 98 | 99 | return includes; 100 | } 101 | 102 | public findPackageExecutables(packageName: string): Promise { 103 | let command: string; 104 | if (process.platform === "win32") { 105 | return this._findPackageFiles(packageName, `--libexec`, `*.exe`); 106 | } else { 107 | const dirs = `catkin_find --without-underlays --libexec --share '${packageName}'`; 108 | command = `find -L $(${dirs}) -type f -executable`; 109 | return new Promise((c, e) => child_process.exec(command, { env: this.env }, (err, out) => 110 | err ? e(err) : c(out.trim().split(os.EOL)), 111 | )); 112 | } 113 | } 114 | 115 | public findPackageLaunchFiles(packageName: string): Promise { 116 | let command: string; 117 | if (process.platform === "win32") { 118 | return this._findPackageFiles(packageName, `--share`, `*.launch`); 119 | } else { 120 | const dirs = `catkin_find --without-underlays --share '${packageName}'`; 121 | command = `find -L $(${dirs}) -type f -name *.launch`; 122 | } 123 | 124 | return new Promise((c, e) => child_process.exec(command, { env: this.env }, (err, out) => { 125 | err ? e(err) : c(out.trim().split(os.EOL)); 126 | })); 127 | } 128 | 129 | public findPackageTestFiles(packageName: string): Promise { 130 | let command: string; 131 | if (process.platform === "win32") { 132 | return this._findPackageFiles(packageName, `--share`, `*.test`); 133 | } else { 134 | const dirs = `catkin_find --without-underlays --share '${packageName}'`; 135 | command = `find -L $(${dirs}) -type f -name *.test`; 136 | } 137 | 138 | return new Promise((c, e) => child_process.exec(command, { env: this.env }, (err, out) => { 139 | err ? e(err) : c(out.trim().split(os.EOL)); 140 | })); 141 | } 142 | 143 | public startCore() { 144 | if (typeof this.env.ROS_MASTER_URI === "undefined") { 145 | return; 146 | } 147 | ros_core.startCore(this.context); 148 | } 149 | 150 | public stopCore() { 151 | if (typeof this.env.ROS_MASTER_URI === "undefined") { 152 | return; 153 | } 154 | ros_core.stopCore(this.context, this._getXmlRpcApi()); 155 | } 156 | 157 | public getCoreStatus(): Promise { 158 | return this._getXmlRpcApi().check(); 159 | } 160 | 161 | public activateCoreMonitor(): vscode.Disposable { 162 | if (typeof this.env.ROS_MASTER_URI === "undefined") { 163 | return null; 164 | } 165 | const coreStatusItem = new ros_core.StatusBarItem(this._getXmlRpcApi()); 166 | coreStatusItem.activate(); 167 | return coreStatusItem; 168 | } 169 | 170 | public rosdep(): vscode.Terminal { 171 | const terminal = ros_utils.createTerminal(this.context); 172 | this.setTerminalEnv(terminal,this.env); 173 | terminal.sendText(`rosdep install --from-paths src --ignore-src -r -y`); 174 | return terminal; 175 | } 176 | 177 | 178 | public showCoreMonitor() { 179 | ros_core.launchMonitor(this.context); 180 | } 181 | 182 | public activateRosrun(packageName: string, executableName: string, argument: string): vscode.Terminal { 183 | const terminal = ros_utils.createTerminal(this.context); 184 | this.setTerminalEnv(terminal,this.env); 185 | terminal.sendText(`rosrun ${packageName} ${executableName} ${argument}`); 186 | return terminal; 187 | } 188 | 189 | public activateRoslaunch(launchFilepath: string, argument: string): vscode.Terminal { 190 | const terminal = ros_utils.createTerminal(this.context); 191 | this.setTerminalEnv(terminal,this.env); 192 | terminal.sendText(`roslaunch ${launchFilepath} ${argument}`); 193 | return terminal; 194 | } 195 | 196 | public activateRostest(launchFilepath: string, argument: string): vscode.Terminal { 197 | const terminal = ros_utils.createTerminal(this.context); 198 | this.setTerminalEnv(terminal,this.env); 199 | terminal.sendText(`rostest ${launchFilepath} ${argument}`); 200 | return terminal; 201 | } 202 | 203 | public setTerminalEnv(terminal:vscode.Terminal,env: any) { 204 | if (process.platform === "linux"){ 205 | for(var item in env){ 206 | terminal.sendText(`export ${item}=${env[item]} >/dev/null`); 207 | } 208 | } 209 | } 210 | 211 | private _findPackageFiles(packageName: string, filter: string, pattern: string): Promise { 212 | return new Promise((c, e) => child_process.exec(`catkin_find --without-underlays ${filter} ${packageName}`, 213 | { env: this.env }, (err, out) => { 214 | const findFilePromises = []; 215 | const paths = out.trim().split(os.EOL); 216 | paths.forEach((foundPath) => { 217 | const normalizedPath = path.win32.normalize(foundPath); 218 | findFilePromises.push(new Promise((found) => child_process.exec( 219 | `where /r "${normalizedPath}" ` + pattern, { env: this.env }, (innerErr, innerOut) => 220 | innerErr ? found(null) : found(innerOut.trim().split(os.EOL)), 221 | ))); 222 | }); 223 | 224 | return Promise.all(findFilePromises).then((values) => { 225 | // remove null elements 226 | values = values.filter((s) => s != null) as string[]; 227 | 228 | // flatten 229 | values = [].concat(...values); 230 | c(values); 231 | }); 232 | }, 233 | )); 234 | } 235 | 236 | private _getXmlRpcApi(): ros_core.XmlRpcApi { 237 | if (this.xmlRpcApi === null) { 238 | this.xmlRpcApi = new ros_core.XmlRpcApi(this.env.ROS_MASTER_URI); 239 | } 240 | return this.xmlRpcApi; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/ros/ros1/webview/ros1_webview_main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | function removeAllChildElements(e) { 4 | while (e.firstChild) { 5 | e.removeChild(e.firstChild); 6 | } 7 | }; 8 | 9 | function generateParemetersUnorderedList(parameters) { 10 | let ul = document.createElement("ul"); 11 | for (let p in parameters) { 12 | if (!parameters.hasOwnProperty(p)) { 13 | continue; 14 | } 15 | 16 | let li = document.createElement("li"); 17 | let name = document.createElement("span"); 18 | name.setAttribute("class", "name"); 19 | name.appendChild(document.createTextNode(p.trim().toString() + ": ")); 20 | li.appendChild(name); 21 | if (parameters[p] instanceof Object) { 22 | li.appendChild(generateParemetersUnorderedList(parameters[p])); 23 | } 24 | else { 25 | li.appendChild(document.createTextNode(parameters[p].toString().trim())); 26 | } 27 | ul.appendChild(li); 28 | } 29 | return ul; 30 | } 31 | 32 | function generateTopicsTable(publishers, subscribers) { 33 | let t = document.createElement("table"); 34 | let th = document.createElement("thead"); 35 | let headerRow = document.createElement("tr"); 36 | let headers = [ 37 | "Name", 38 | "Publishers", 39 | "Subscribers", 40 | ]; 41 | headers.forEach((name, _i) => { 42 | let h = document.createElement("th"); 43 | h.appendChild(document.createTextNode(name)); 44 | headerRow.appendChild(h); 45 | }); 46 | 47 | th.appendChild(headerRow); 48 | t.appendChild(th); 49 | 50 | let tb = document.createElement("tbody"); 51 | let topics = Object.keys({ 52 | ...publishers, 53 | ...subscribers, 54 | }); 55 | console.debug(topics); 56 | for (let i in topics) { 57 | let topic = topics[i]; 58 | let r = document.createElement("tr"); 59 | let name = document.createElement("td"); 60 | name.appendChild(document.createTextNode(topic)); 61 | 62 | let publisher = document.createElement("td"); 63 | publisher.appendChild(document.createTextNode(publishers.hasOwnProperty(topic) ? publishers[topic] : "")); 64 | 65 | let subscriber = document.createElement("td"); 66 | subscriber.appendChild(document.createTextNode(subscribers.hasOwnProperty(topic) ? subscribers[topic] : "")); 67 | 68 | r.appendChild(name); 69 | r.appendChild(publisher); 70 | r.appendChild(subscriber); 71 | tb.appendChild(r); 72 | } 73 | t.appendChild(tb); 74 | 75 | return t; 76 | } 77 | 78 | function generateServicesTable(services) { 79 | let t = document.createElement("table"); 80 | 81 | let th = document.createElement("thead"); 82 | let headerRow = document.createElement("tr"); 83 | let headers = [ 84 | "Name", 85 | "Providers", 86 | ]; 87 | headers.forEach((name, _i) => { 88 | let h = document.createElement("th"); 89 | h.appendChild(document.createTextNode(name)); 90 | headerRow.appendChild(h); 91 | }); 92 | th.appendChild(headerRow); 93 | t.appendChild(th); 94 | 95 | let tb = document.createElement("tbody"); 96 | for (let s in services) { 97 | if (!services.hasOwnProperty(s)) { 98 | continue; 99 | } 100 | 101 | let r = document.createElement("tr"); 102 | let name = document.createElement("td"); 103 | name.appendChild(document.createTextNode(s)); 104 | 105 | let providers = document.createElement("td"); 106 | providers.appendChild(document.createTextNode(services[s].join(", "))); 107 | 108 | r.appendChild(name); 109 | r.appendChild(providers); 110 | tb.appendChild(r); 111 | } 112 | t.appendChild(tb); 113 | 114 | return t; 115 | } 116 | 117 | function initializeCoreMonitor() { 118 | // handle message passed from extension to webview 119 | window.addEventListener("message", (event) => { 120 | const message = event.data; 121 | console.debug(message); 122 | 123 | const coreStatus = document.getElementById("ros-status"); 124 | const parametersElement = document.getElementById("parameters"); 125 | const topicsElement = document.getElementById("topics"); 126 | const servicesElement = document.getElementById("services"); 127 | 128 | removeAllChildElements(parametersElement); 129 | removeAllChildElements(topicsElement); 130 | removeAllChildElements(servicesElement); 131 | 132 | if (message.status) { 133 | coreStatus.textContent = "online"; 134 | 135 | let parameters = JSON.parse(message.parameters); 136 | let systemState = JSON.parse(message.systemState); 137 | 138 | let parametersHeader = document.createElement("h2"); 139 | parametersHeader.appendChild(document.createTextNode("Parameters")); 140 | parametersElement.appendChild(parametersHeader); 141 | parametersElement.appendChild(generateParemetersUnorderedList(parameters)); 142 | 143 | let topicsHeader = document.createElement("h2"); 144 | topicsHeader.appendChild(document.createTextNode("Topics")); 145 | topicsElement.appendChild(topicsHeader); 146 | topicsElement.appendChild(generateTopicsTable(systemState.publishers, systemState.subscribers)); 147 | let servicesHeader = document.createElement("h2"); 148 | servicesHeader.appendChild(document.createTextNode("Services")); 149 | servicesElement.appendChild(servicesHeader); 150 | servicesElement.appendChild(generateServicesTable(systemState.services)); 151 | } 152 | else { 153 | coreStatus.textContent = "offline"; 154 | } 155 | }); 156 | }; 157 | 158 | window.onload = () => initializeCoreMonitor(); 159 | -------------------------------------------------------------------------------- /src/ros/ros2/daemon.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as util from "util"; 6 | import * as vscode from "vscode"; 7 | 8 | import * as extension from "../../extension"; 9 | import * as ros2_monitor from "./ros2-monitor" 10 | import { env } from "../../extension"; 11 | 12 | /** 13 | * start the ROS2 daemon. 14 | */ 15 | export async function startDaemon() { 16 | const command: string = "ros2 daemon start"; 17 | const exec = util.promisify(child_process.exec); 18 | extension.outputChannel.appendLine("Attempting to start daemon with " + command); 19 | await exec(command, { env: env }); 20 | } 21 | 22 | /** 23 | * stop the ROS2 daemon. 24 | */ 25 | export async function stopDaemon() { 26 | const command: string = "ros2 daemon stop"; 27 | const exec = util.promisify(child_process.exec); 28 | await exec(command, { env: env }); 29 | } 30 | 31 | /** 32 | * Shows the ROS core status in the status bar. 33 | */ 34 | export class StatusBarItem { 35 | private item: vscode.StatusBarItem; 36 | private timeout: NodeJS.Timeout; 37 | private ros2cli: ros2_monitor.XmlRpcApi; 38 | 39 | public constructor() { 40 | this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 200); 41 | 42 | const waitIcon = "$(clock)"; 43 | const ros = "ROS"; 44 | this.item.text = `${waitIcon} ${ros}`; 45 | this.item.command = extension.Commands.ShowCoreStatus; 46 | this.ros2cli = new ros2_monitor.XmlRpcApi(); 47 | } 48 | 49 | public activate() { 50 | this.item.show(); 51 | this.timeout = setTimeout(() => this.update(), 200); 52 | } 53 | 54 | public dispose() { 55 | clearTimeout(this.timeout); 56 | this.item.dispose(); 57 | } 58 | 59 | private async update() { 60 | let status: boolean = false; 61 | try { 62 | const result = await this.ros2cli.getNodeNamesAndNamespaces(); 63 | status = true; 64 | } catch (error) { 65 | // Do nothing 66 | } finally { 67 | const statusIcon = status ? "$(check)" : "$(x)"; 68 | let ros = "ROS"; 69 | 70 | // these environment variables are set by the ros_environment package 71 | // https://github.com/ros/ros_environment 72 | const rosVersionChecker = "ROS_VERSION"; 73 | const rosDistroChecker = "ROS_DISTRO"; 74 | if (rosVersionChecker in extension.env && rosDistroChecker in extension.env) { 75 | const rosVersion: string = extension.env[rosVersionChecker]; 76 | const rosDistro: string = extension.env[rosDistroChecker]; 77 | ros += `${rosVersion}.${rosDistro}`; 78 | } 79 | this.item.text = `${statusIcon} ${ros}`; 80 | this.timeout = setTimeout(() => this.update(), 200); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ros/ros2/ros2-monitor.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as path from "path"; 5 | import * as vscode from "vscode"; 6 | import * as xmlrpc from "xmlrpc"; 7 | 8 | import * as extension from "../../extension"; 9 | import * as telemetry from "../../telemetry-helper"; 10 | 11 | function getDaemonPort() { 12 | let basePort: number = 11511; 13 | basePort += Number(process.env.ROS_DOMAIN_ID) || 0 14 | return basePort; 15 | } 16 | 17 | function getDaemonUri() { 18 | return `http://localhost:${getDaemonPort()}/ros2cli/`; 19 | } 20 | 21 | export function launchMonitor(context: vscode.ExtensionContext) { 22 | const reporter = telemetry.getReporter(); 23 | reporter.sendTelemetryCommand(extension.Commands.ShowCoreStatus); 24 | 25 | const panel = vscode.window.createWebviewPanel( 26 | "ros2Status", 27 | "ROS2 Status", 28 | vscode.ViewColumn.Two, 29 | { 30 | enableScripts: true, 31 | } 32 | ); 33 | 34 | const stylesheet = panel.webview.asWebviewUri(vscode.Uri.file(path.join(context.extensionPath, "assets", "ros", "core-monitor", "style.css"))); 35 | 36 | const script = panel.webview.asWebviewUri(vscode.Uri.file(path.join(context.extensionPath, "dist", "ros2_webview_main.js"))); 37 | 38 | panel.webview.html = getCoreStatusWebviewContent(stylesheet, script); 39 | 40 | const ros2cliApi = new XmlRpcApi(); 41 | const pollingHandle = setInterval(async () => { 42 | try { 43 | const result: any[] = await Promise.all([ 44 | ros2cliApi.getNodeNamesAndNamespaces(), 45 | ros2cliApi.getTopicNamesAndTypes(), 46 | ros2cliApi.getServiceNamesAndTypes()]); 47 | const nodesJSON = JSON.stringify(result[0]); 48 | const topicsJSON = JSON.stringify(result[1]); 49 | const servicesJSON = JSON.stringify(result[2]); 50 | panel.webview.postMessage({ 51 | ready: true, 52 | nodes: nodesJSON, 53 | topics: topicsJSON, 54 | services: servicesJSON, 55 | }); 56 | } catch (e) { 57 | panel.webview.postMessage({ 58 | ready: false, 59 | }); 60 | } 61 | }, 200); 62 | 63 | panel.onDidDispose(() => { 64 | clearInterval(pollingHandle); 65 | }); 66 | } 67 | 68 | function getCoreStatusWebviewContent(stylesheet: vscode.Uri, script: vscode.Uri): string { 69 | return ` 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |

ROS2 System Status

81 |

-

82 | 83 |
84 |
85 |
86 | 87 | 88 | 89 | `; 90 | } 91 | 92 | /** 93 | * ros2cli xmlrpc interfaces. 94 | */ 95 | export class XmlRpcApi { 96 | private client: xmlrpc.Client; 97 | 98 | public constructor() { 99 | this.client = xmlrpc.createClient(getDaemonUri()); 100 | } 101 | 102 | public check() : Promise { 103 | // the ROS2 CLI doesn't have an API which returns detailed status, 104 | // so we're just using another endpoint to verify it is running 105 | return this.methodCall("get_node_names_and_namespaces").then(() => true, () => false); 106 | } 107 | 108 | public getNodeNamesAndNamespaces() : Promise { 109 | return this.methodCall("get_node_names_and_namespaces"); 110 | } 111 | 112 | public getServiceNamesAndTypes() : Promise { 113 | return this.methodCall("get_service_names_and_types"); 114 | } 115 | 116 | public getTopicNamesAndTypes() : Promise { 117 | return this.methodCall("get_topic_names_and_types"); 118 | } 119 | 120 | private methodCall(method: string, ...args: any[]): Promise { 121 | return new Promise((resolve, reject) => { 122 | this.client.methodCall(method, [...args], (err, val) => { 123 | if (err) { 124 | reject(err); 125 | } else { 126 | resolve(val); 127 | } 128 | }); 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/ros/ros2/ros2.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import * as path from "path"; 8 | import * as util from "util"; 9 | import * as vscode from "vscode"; 10 | 11 | import * as ros from "../ros"; 12 | import * as daemon from "./daemon"; 13 | import * as ros2_monitor from "./ros2-monitor"; 14 | import * as ros_utils from "../utils"; 15 | 16 | const promisifiedExists = util.promisify(fs.exists); 17 | const promisifiedExec = util.promisify(child_process.exec); 18 | 19 | export class ROS2 implements ros.ROSApi { 20 | private context: vscode.ExtensionContext; 21 | private env: any; 22 | 23 | public setContext(context: vscode.ExtensionContext, env: any) { 24 | this.context = context; 25 | this.env = env; 26 | } 27 | 28 | public getPackageNames(): Promise { 29 | return new Promise((resolve, reject) => child_process.exec("ros2 pkg list", { env: this.env }, (err, out) => { 30 | if (!err) { 31 | const lines = out.trim().split(os.EOL).map(((line) => { 32 | return line; 33 | })); 34 | resolve(lines); 35 | } else { 36 | reject(err); 37 | } 38 | })); 39 | } 40 | 41 | public async getPackages(): Promise<{ [name: string]: () => Promise }> { 42 | const packages: { [name: string]: () => Promise } = {}; 43 | const { stdout } = child_process.exec("ros2 pkg list", { env: this.env }); 44 | let chucks = ""; 45 | for await (const chuck of stdout) { 46 | chucks += chuck; 47 | } 48 | 49 | chucks.split(os.EOL).map(((line) => { 50 | const packageName: string = line.trim(); 51 | packages[packageName] = async (): Promise => { 52 | const { stdout } = await child_process.exec( 53 | `ros2 pkg prefix --share ${packageName}`, { env: this.env }); 54 | let innerChucks = ""; 55 | for await (const chuck of stdout) { 56 | innerChucks += chuck; 57 | } 58 | return innerChucks.trim(); 59 | }; 60 | })); 61 | 62 | return packages; 63 | } 64 | 65 | public async getIncludeDirs(): Promise { 66 | const prefixPaths: string[] = []; 67 | if (this.env.AMENT_PREFIX_PATH) { 68 | prefixPaths.push(...this.env.AMENT_PREFIX_PATH.split(path.delimiter)); 69 | } 70 | 71 | const includeDirs: string[] = []; 72 | for (const dir of prefixPaths) { 73 | const include = path.join(dir, "include"); 74 | if (await promisifiedExists(include)) { 75 | includeDirs.push(include); 76 | } 77 | } 78 | 79 | return includeDirs; 80 | } 81 | 82 | public async getWorkspaceIncludeDirs(workspaceDir: string): Promise { 83 | const includes: string[] = []; 84 | const opts = { 85 | env: this.env, 86 | cwd: workspaceDir 87 | }; 88 | const isWin = process.platform === "win32"; 89 | const nullPath = isWin ? "nul" : "/dev/null"; 90 | const result = await promisifiedExec(`colcon list -p --base-paths "${workspaceDir}" --log-base ${nullPath}`, opts); 91 | 92 | // error out if we see anything from stderr. 93 | if (result.stderr) { 94 | return includes; 95 | } 96 | 97 | // each line should be a path like `c:\ros2_ws\src\demos\demo_nodes_cpp` 98 | for (const line of result.stdout.split(os.EOL)) { 99 | const include = path.join(line, "include"); 100 | 101 | if (await promisifiedExists(include)) { 102 | includes.push(include); 103 | } 104 | } 105 | 106 | return includes; 107 | } 108 | 109 | public findPackageExecutables(packageName: string): Promise { 110 | return new Promise((resolve, reject) => child_process.exec( 111 | `ros2 pkg executables ${packageName}`, { env: this.env }, (err, out) => { 112 | if (!err) { 113 | const lines = out.trim().split(os.EOL).map(((line) => { 114 | const info: string[] = line.split(" "); 115 | if (info.length === 2) { 116 | // each line should contain exactly 2 strings separated by 1 space 117 | return info; 118 | } 119 | })); 120 | 121 | const packageInfoReducer = (acc: string[], cur: string[]) => { 122 | const executableName: string = cur[1] as string; 123 | acc.push(executableName); 124 | return acc; 125 | }; 126 | resolve(lines.reduce(packageInfoReducer, [])); 127 | } else { 128 | reject(err); 129 | } 130 | })); 131 | } 132 | 133 | public async findPackageLaunchFiles(packageName: string): Promise { 134 | const packages = await this.getPackages(); 135 | const packageBasePath = await packages[packageName](); 136 | const command: string = (process.platform === "win32") ? 137 | `where /r "${packageBasePath}" *launch.py` : 138 | `find -L "${packageBasePath}" -type f -name *launch.py`; 139 | 140 | return new Promise((c, e) => child_process.exec(command, { env: this.env }, (err, out) => { 141 | err ? e(new Error('No launch files are found.')) : c(out.trim().split(os.EOL)); 142 | })); 143 | } 144 | 145 | public async findPackageTestFiles(packageName: string): Promise { 146 | // TODO: ROS2 rostest equivalent not implemented yet 147 | return new Promise((resolve, reject) => { 148 | resolve([]); 149 | }); 150 | } 151 | 152 | public async startCore() { 153 | daemon.startDaemon(); 154 | } 155 | 156 | public async stopCore() { 157 | daemon.stopDaemon(); 158 | } 159 | 160 | public async getCoreStatus(): Promise { 161 | const ros2cliApi = new ros2_monitor.XmlRpcApi(); 162 | return ros2cliApi.check(); 163 | } 164 | 165 | public rosdep(): vscode.Terminal { 166 | const terminal = ros_utils.createTerminal(this.context); 167 | terminal.sendText(`rosdep install --from-paths src --ignore-src -r -y`); 168 | return terminal; 169 | } 170 | 171 | public activateCoreMonitor(): vscode.Disposable { 172 | const coreStatusItem = new daemon.StatusBarItem(); 173 | coreStatusItem.activate(); 174 | return coreStatusItem; 175 | } 176 | 177 | public async showCoreMonitor() { 178 | return ros2_monitor.launchMonitor(this.context); 179 | } 180 | 181 | public activateRosrun(packageName: string, executableName: string, argument: string): vscode.Terminal { 182 | const terminal = ros_utils.createTerminal(this.context); 183 | terminal.sendText(`ros2 run ${packageName} ${executableName} ${argument}`); 184 | return terminal; 185 | } 186 | 187 | public activateRoslaunch(launchFilepath: string, argument: string): vscode.Terminal { 188 | const terminal = ros_utils.createTerminal(this.context); 189 | terminal.sendText(`ros2 launch ${launchFilepath} ${argument}`); 190 | return terminal; 191 | } 192 | 193 | public activateRostest(launchFilepath: string, argument: string): vscode.Terminal { 194 | console.error("ROS2 rostest equivalent not implemented yet"); 195 | return; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/ros/ros2/webview/ros2_webview_main.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace ros2monitor { 5 | function removeAllChildElements(e) { 6 | while (e.firstChild) { 7 | e.removeChild(e.firstChild); 8 | } 9 | }; 10 | 11 | function generateColumnTable(dataArray: any, headers: string[], callback: (data: any, i: number) => string) { 12 | let t = document.createElement("table"); 13 | let th = document.createElement("thead"); 14 | let headerRow = document.createElement("tr"); 15 | headers.forEach((name, _i) => { 16 | let h = document.createElement("th"); 17 | h.appendChild(document.createTextNode(name)); 18 | headerRow.appendChild(h); 19 | }); 20 | 21 | th.appendChild(headerRow); 22 | t.appendChild(th); 23 | 24 | let tb = document.createElement("tbody"); 25 | for (const i in dataArray) { 26 | const data = dataArray[i]; 27 | const r = document.createElement("tr"); 28 | headers.forEach((name, _i) => { 29 | let cell = document.createElement("td"); 30 | cell.appendChild(document.createTextNode(callback(data, _i))); 31 | r.appendChild(cell); 32 | }); 33 | tb.appendChild(r); 34 | } 35 | t.appendChild(tb); 36 | 37 | return t; 38 | } 39 | 40 | export function initializeRos2Monitor() { 41 | // handle message passed from extension to webview 42 | window.addEventListener("message", (event) => { 43 | const message = event.data; 44 | 45 | const coreStatus = document.getElementById("ros-status"); 46 | const topicsElement = document.getElementById("topics"); 47 | const servicesElement = document.getElementById("services"); 48 | 49 | removeAllChildElements(topicsElement); 50 | removeAllChildElements(servicesElement); 51 | 52 | if (message.ready) { 53 | coreStatus.textContent = "online"; 54 | 55 | const nodes = JSON.parse(message.nodes); 56 | const topics = JSON.parse(message.topics); 57 | const services = JSON.parse(message.services); 58 | 59 | const nodesHeader = document.createElement("h2"); 60 | nodesHeader.appendChild(document.createTextNode("Nodes")); 61 | topicsElement.appendChild(nodesHeader); 62 | topicsElement.appendChild(generateColumnTable(nodes, ["Name"], (data, i) => { 63 | return `${data[1]}${data[0]}`; 64 | })); 65 | 66 | const topicsHeader = document.createElement("h2"); 67 | topicsHeader.appendChild(document.createTextNode("Topics")); 68 | topicsElement.appendChild(topicsHeader); 69 | topicsElement.appendChild(generateColumnTable(topics, ["Name", "Type"], (data, i) => { 70 | return data[i]; 71 | })); 72 | 73 | const servicesHeader = document.createElement("h2"); 74 | servicesHeader.appendChild(document.createTextNode("Services")); 75 | servicesElement.appendChild(servicesHeader); 76 | servicesElement.appendChild(generateColumnTable(services, ["Name", "Type"], (data, i) => { 77 | return data[i]; 78 | })); 79 | } 80 | else { 81 | coreStatus.textContent = "offline"; 82 | } 83 | }); 84 | }; 85 | } 86 | 87 | window.onload = () => ros2monitor.initializeRos2Monitor(); 88 | -------------------------------------------------------------------------------- /src/ros/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Andrew Short. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as child_process from "child_process"; 5 | import * as os from "os"; 6 | import * as vscode from "vscode"; 7 | 8 | import * as extension from "../extension"; 9 | import * as pfs from "../promise-fs"; 10 | import * as telemetry from "../telemetry-helper"; 11 | 12 | /** 13 | * Executes a setup file and returns the resulting env. 14 | */ 15 | export function sourceSetupFile(filename: string, env?: any): Promise { 16 | return new Promise((resolve, reject) => { 17 | let exportEnvCommand: string; 18 | if (process.platform === "win32") { 19 | exportEnvCommand = `cmd /c "\"${filename}\" && set"`; 20 | } 21 | else { 22 | // Force login shell, so ROS sources correctly in containers. 23 | exportEnvCommand = `bash --login -c "source '${filename}' && env"`; 24 | extension.outputChannel.appendLine("Sourcing Environment using: " + exportEnvCommand); 25 | } 26 | 27 | let processOptions: child_process.ExecOptions = { 28 | cwd: vscode.workspace.rootPath, 29 | env: env, 30 | }; 31 | child_process.exec(exportEnvCommand, processOptions, (error, stdout, _stderr) => { 32 | if (!error) { 33 | resolve(stdout.split(os.EOL).reduce((env, line) => { 34 | const index = line.indexOf("="); 35 | 36 | if (index !== -1) { 37 | env[line.substr(0, index)] = line.substr(index + 1); 38 | } 39 | 40 | return env; 41 | }, {})); 42 | } else { 43 | reject(error); 44 | } 45 | }); 46 | }); 47 | } 48 | 49 | export function xacro(filename: string): Promise { 50 | return new Promise((resolve, reject) => { 51 | let processOptions = { 52 | cwd: vscode.workspace.rootPath, 53 | env: extension.env, 54 | windowsHide: false, 55 | }; 56 | 57 | let xacroCommand: string; 58 | if (process.platform === "win32") { 59 | xacroCommand = `cmd /c "xacro "${filename}""`; 60 | } else { 61 | xacroCommand = `bash --login -c "xacro '${filename}' && env"`; 62 | } 63 | 64 | child_process.exec(xacroCommand, processOptions, (error, stdout, _stderr) => { 65 | if (!error) { 66 | resolve(stdout); 67 | } else { 68 | reject(error); 69 | } 70 | }); 71 | }); 72 | } 73 | 74 | /** 75 | * Gets the names of installed distros. 76 | */ 77 | export function getDistros(): Promise { 78 | return pfs.readdir("/opt/ros"); 79 | } 80 | 81 | /** 82 | * Creates and shows a ROS-sourced terminal. 83 | */ 84 | export function createTerminal(context: vscode.ExtensionContext): vscode.Terminal { 85 | const reporter = telemetry.getReporter(); 86 | reporter.sendTelemetryCommand(extension.Commands.CreateTerminal); 87 | 88 | const terminal = vscode.window.createTerminal({ name: 'ROS', env: extension.env }) 89 | terminal.show(); 90 | 91 | return terminal; 92 | } 93 | -------------------------------------------------------------------------------- /src/telemetry-helper.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import TelemetryReporter from "@vscode/extension-telemetry"; 5 | 6 | import * as vscode_utils from "./vscode-utils"; 7 | 8 | let reporterSingleton: TelemetryReporter; 9 | 10 | function getTelemetryReporter(): TelemetryReporter { 11 | if (reporterSingleton) { 12 | return reporterSingleton; 13 | } 14 | 15 | const extensionId = "ms-iot.vscode-ros"; 16 | const packageInfo = vscode_utils.getPackageInfo(extensionId); 17 | if (packageInfo) { 18 | reporterSingleton = new TelemetryReporter(packageInfo.name, packageInfo.version, packageInfo.aiKey); 19 | } 20 | return reporterSingleton; 21 | } 22 | 23 | enum TelemetryEvent { 24 | activate = "activate", 25 | command = "command", 26 | } 27 | 28 | export interface ITelemetryReporter { 29 | sendTelemetryActivate(): void; 30 | sendTelemetryCommand(commandName: string): void; 31 | } 32 | 33 | class SimpleReporter implements ITelemetryReporter { 34 | private telemetryReporter: TelemetryReporter; 35 | 36 | constructor() { 37 | this.telemetryReporter = getTelemetryReporter(); 38 | } 39 | 40 | public sendTelemetryActivate(): void { 41 | if (!this.telemetryReporter) { 42 | return; 43 | } 44 | this.telemetryReporter.sendTelemetryEvent(TelemetryEvent.activate); 45 | } 46 | 47 | public sendTelemetryCommand(commandName: string): void { 48 | if (!this.telemetryReporter) { 49 | return; 50 | } 51 | this.telemetryReporter.sendTelemetryEvent(TelemetryEvent.command, { 52 | name: commandName, 53 | }); 54 | } 55 | } 56 | 57 | export function getReporter(): ITelemetryReporter { 58 | return (new SimpleReporter()); 59 | } 60 | 61 | export async function clearReporter(): Promise { 62 | await reporterSingleton.dispose(); 63 | reporterSingleton = undefined; 64 | } 65 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.equal([1, 2, 3].indexOf(5), -1); 13 | assert.equal([1, 2, 3].indexOf(0), -1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | 11 | const testsRoot = path.resolve(__dirname, '..'); 12 | 13 | return new Promise((c, e) => { 14 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 15 | if (err) { 16 | return e(err); 17 | } 18 | 19 | // Add files to the test suite 20 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 21 | 22 | try { 23 | // Run the mocha test 24 | mocha.run(failures => { 25 | if (failures > 0) { 26 | e(new Error(`${failures} tests failed.`)); 27 | } else { 28 | c(); 29 | } 30 | }); 31 | } catch (err) { 32 | console.error(err); 33 | e(err); 34 | } 35 | }); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/urdfPreview/URDFPreviewPanel.ts: -------------------------------------------------------------------------------- 1 | import * as BABYLON from 'babylonjs'; 2 | import * as Materials from 'babylonjs-materials'; 3 | import * as urdf from '@polyhobbyist/babylon_ros'; 4 | import * as ColladaFileLoader from '@polyhobbyist/babylon-collada-loader'; 5 | import * as GUI from 'babylonjs-gui'; 6 | 7 | // Get access to the VS Code API from within the webview context 8 | const vscode = acquireVsCodeApi(); 9 | 10 | const canvas = document.getElementById("renderCanvas") as HTMLCanvasElement; // Get the canvas element 11 | const engine = new BABYLON.Engine(canvas, true); // Generate the BABYLON 3D engine 12 | let scene : BABYLON.Scene | undefined = undefined; 13 | 14 | let currentRobot : urdf.Robot | undefined = undefined; 15 | 16 | let ground : BABYLON.GroundMesh | undefined = undefined; 17 | let camera : BABYLON.ArcRotateCamera | undefined = undefined; 18 | let statusLabel = new GUI.TextBlock(); 19 | 20 | function clearStatus() { 21 | statusLabel.text = ""; 22 | } 23 | 24 | let readyToRender : Boolean = false; 25 | 26 | let jointAxisList : BABYLON.PositionGizmo[] = []; 27 | let linkAxisList : BABYLON.PositionGizmo[] = []; 28 | 29 | 30 | function clearAxisGizmos() { 31 | linkAxisList.forEach((a) => { 32 | a.dispose(); 33 | }); 34 | linkAxisList = []; 35 | 36 | jointAxisList.forEach((a) => { 37 | a.dispose(); 38 | }); 39 | jointAxisList = []; 40 | } 41 | 42 | function addAxisToTransform(list : BABYLON.PositionGizmo[], scene : BABYLON.Scene, layer: BABYLON.UtilityLayerRenderer, transform : BABYLON.TransformNode | undefined) { 43 | if (transform) { 44 | let axis = new BABYLON.PositionGizmo(layer); 45 | axis.scaleRatio = 0.5 46 | axis.attachedNode = transform; 47 | list.push(axis); 48 | 49 | let drag = () => { 50 | if (transform) { 51 | statusLabel.text = transform.name + 52 | "\nX: " + transform.position.x.toFixed(6) + 53 | "\nY: " + transform.position.y.toFixed(6) + 54 | "\nZ: " + transform.position.z.toFixed(6); 55 | statusLabel.linkOffsetY = -100; 56 | statusLabel.linkWithMesh(transform); 57 | } 58 | } 59 | 60 | axis.xGizmo.dragBehavior.onDragObservable.add(drag); 61 | axis.yGizmo.dragBehavior.onDragObservable.add(drag); 62 | axis.zGizmo.dragBehavior.onDragObservable.add(drag); 63 | 64 | } 65 | } 66 | 67 | function toggleAxisOnRobot(jointOrLink : Boolean, scene : BABYLON.Scene, layer: BABYLON.UtilityLayerRenderer, robot : urdf.Robot) { 68 | let whichAxis = jointOrLink ? jointAxisList : linkAxisList; 69 | 70 | if (whichAxis.length == 0) { 71 | if (jointOrLink) { 72 | robot.joints.forEach((j) => { 73 | addAxisToTransform(whichAxis, scene, layer, j.transform); 74 | }); 75 | } else { 76 | robot.links.forEach((l) => { 77 | l.visuals.forEach((v) => { 78 | addAxisToTransform(whichAxis, scene, layer, v.transform); 79 | }); 80 | }); 81 | } 82 | } else { 83 | clearAxisGizmos(); 84 | clearStatus(); 85 | } 86 | } 87 | 88 | let jointRotationGizmos : BABYLON.RotationGizmo[] = []; 89 | let linkRotationGizmos : BABYLON.RotationGizmo[] = []; 90 | 91 | function clearRotationGizmos() { 92 | jointRotationGizmos.forEach((a) => { 93 | a.dispose(); 94 | }); 95 | jointRotationGizmos = []; 96 | linkRotationGizmos.forEach((a) => { 97 | a.dispose(); 98 | }); 99 | linkRotationGizmos = []; 100 | } 101 | 102 | function addRotationToTransform(list : BABYLON.RotationGizmo[], scene : BABYLON.Scene, layer: BABYLON.UtilityLayerRenderer, transform : BABYLON.TransformNode | undefined) { 103 | if (transform) { 104 | let rotationGizmo = new BABYLON.RotationGizmo(layer); 105 | rotationGizmo.scaleRatio = 0.5 106 | rotationGizmo.attachedNode = transform; 107 | list.push(rotationGizmo); 108 | 109 | let drag = () => { 110 | if (transform) { 111 | statusLabel.text = transform.name + 112 | "\nR:" + transform.rotation.x.toFixed(6) + 113 | "\nP:" + transform.rotation.y.toFixed(6) + 114 | "\nY:" + transform.rotation.z.toFixed(6); 115 | statusLabel.linkOffsetY = -100; 116 | statusLabel.linkWithMesh(transform); 117 | } 118 | } 119 | 120 | rotationGizmo.xGizmo.dragBehavior.onDragObservable.add(drag); 121 | rotationGizmo.yGizmo.dragBehavior.onDragObservable.add(drag); 122 | rotationGizmo.zGizmo.dragBehavior.onDragObservable.add(drag); 123 | } 124 | 125 | } 126 | 127 | function toggleAxisRotationOnRobot(jointOrLink : Boolean, ui: GUI.AdvancedDynamicTexture, scene : BABYLON.Scene, layer: BABYLON.UtilityLayerRenderer, robot : urdf.Robot) { 128 | let whichList = jointOrLink ? jointRotationGizmos : linkRotationGizmos; 129 | if (whichList.length == 0) { 130 | if (jointOrLink) { 131 | robot.joints.forEach((j) => { 132 | addRotationToTransform(whichList, scene, layer, j.transform); 133 | }); 134 | } else { 135 | robot.links.forEach((l) => { 136 | l.visuals.forEach((v) => { 137 | addRotationToTransform(whichList, scene, layer, v.transform); 138 | }); 139 | }); 140 | } 141 | } else { 142 | clearRotationGizmos(); 143 | clearStatus(); 144 | } 145 | } 146 | 147 | 148 | var createScene = async function () { 149 | scene = new BABYLON.Scene(engine); 150 | if (BABYLON.SceneLoader) { 151 | //Add this loader into the register plugin 152 | BABYLON.SceneLoader.RegisterPlugin(new ColladaFileLoader.DAEFileLoader()); 153 | } 154 | 155 | // This creates a basic Babylon Scene object (non-mesh) 156 | // Create a default ground and skybox. 157 | const environment = scene.createDefaultEnvironment({ 158 | createGround: true, 159 | createSkybox: false, 160 | enableGroundMirror: true, 161 | groundMirrorSizeRatio: 0.15 162 | }); 163 | 164 | scene.useRightHandedSystem = true; 165 | scene.clearColor = BABYLON.Color4.FromColor3(BABYLON.Color3.Black()); 166 | 167 | 168 | // This creates and positions a free camera (non-mesh) 169 | camera = new BABYLON.ArcRotateCamera("camera1", - Math.PI / 3, 5 * Math.PI / 12, 1, new BABYLON.Vector3(0, 0, 0), scene); 170 | camera.wheelDeltaPercentage = 0.01; 171 | camera.minZ = 0.1; 172 | 173 | const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene); 174 | 175 | // This attaches the camera to the canvas 176 | camera.attachControl(canvas, true); 177 | 178 | var groundMaterial = new Materials.GridMaterial("groundMaterial", scene); 179 | groundMaterial.majorUnitFrequency = 5; 180 | groundMaterial.minorUnitVisibility = 0.5; 181 | groundMaterial.gridRatio = 1; 182 | groundMaterial.opacity = 0.8; 183 | groundMaterial.useMaxLine = true; 184 | groundMaterial.lineColor = BABYLON.Color3.Green(); 185 | groundMaterial.mainColor = BABYLON.Color3.Green(); 186 | 187 | ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 50, height: 50}, scene); 188 | ground.material = groundMaterial; 189 | ground.isPickable = false; 190 | 191 | 192 | return scene; 193 | }; 194 | 195 | function resetCamera() { 196 | camera.alpha = - Math.PI / 3; 197 | camera.beta = 5 * Math.PI / 12; 198 | camera.target = new BABYLON.Vector3(0, 0, 0); 199 | } 200 | 201 | function createButton(toolbar: GUI.StackPanel, name : string, text : string, scene : BABYLON.Scene, onClick : () => void) { 202 | var button = GUI.Button.CreateSimpleButton(name, text); 203 | button.width = "100px" 204 | button.height = "20px"; 205 | button.color = "white"; 206 | button.cornerRadius = 5; 207 | button.background = "green"; 208 | button.onPointerUpObservable.add(onClick); 209 | toolbar.addControl(button); 210 | return button; 211 | } 212 | 213 | function createUI(scene : BABYLON.Scene) { 214 | var advancedTexture = GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI"); 215 | 216 | statusLabel.color = "white"; 217 | statusLabel.textHorizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; 218 | statusLabel.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_CENTER; 219 | statusLabel.resizeToFit = true; 220 | statusLabel.outlineColor = "green"; 221 | statusLabel.outlineWidth = 2.0; 222 | advancedTexture.addControl(statusLabel); 223 | 224 | var toolbar = new GUI.StackPanel(); 225 | toolbar.paddingTop = "10px"; 226 | toolbar.paddingLeft = "10px"; 227 | toolbar.width = "500px"; 228 | toolbar.height = "50px"; 229 | toolbar.fontSize = "14px"; 230 | toolbar.horizontalAlignment = GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; 231 | toolbar.verticalAlignment = GUI.Control.VERTICAL_ALIGNMENT_TOP; 232 | toolbar.isVertical = false; 233 | advancedTexture.addControl(toolbar); 234 | 235 | 236 | var utilLayer = new BABYLON.UtilityLayerRenderer(scene); 237 | 238 | const gizmoManager = new BABYLON.GizmoManager(scene); 239 | gizmoManager.usePointerToAttachGizmos = false; 240 | 241 | createButton(toolbar, "jointAxisButton", "Joint Axis", scene, () => { 242 | toggleAxisOnRobot(true, scene, utilLayer, currentRobot); 243 | }); 244 | 245 | createButton(toolbar, "linkAxisButton", "Link Axis", scene, () => { 246 | toggleAxisOnRobot(false, scene, utilLayer, currentRobot); 247 | }); 248 | 249 | createButton(toolbar, "jointRotationButton", "Joint Rotation", scene, () => { 250 | toggleAxisRotationOnRobot(true, advancedTexture, scene, utilLayer, currentRobot); 251 | }); 252 | 253 | createButton(toolbar, "linkRotationButton", "Link Rotation", scene, () => { 254 | toggleAxisRotationOnRobot(false, advancedTexture, scene, utilLayer, currentRobot); 255 | }); 256 | 257 | } 258 | 259 | async function applyURDF(urdfText) { 260 | try { 261 | clearAxisGizmos(); 262 | clearRotationGizmos(); 263 | clearStatus(); 264 | resetCamera(); 265 | 266 | if (currentRobot) { 267 | currentRobot.dispose(); 268 | currentRobot = undefined; 269 | } 270 | 271 | vscode.postMessage({ 272 | command: "trace", 273 | text: `loading urdf`, 274 | }); 275 | 276 | 277 | currentRobot = await urdf.deserializeUrdfToRobot(urdfText); 278 | currentRobot.create(scene); 279 | 280 | vscode.postMessage({ 281 | command: "trace", 282 | text: `loaded urdf`, 283 | }); 284 | } catch (err) { 285 | 286 | vscode.postMessage({ 287 | command: "error", 288 | text: `Could not render URDF due to: ${err}\n${err.stack}`, 289 | }); 290 | } 291 | } 292 | 293 | // Main function that gets executed once the webview DOM loads 294 | async function main() { 295 | 296 | scene = await createScene(); 297 | createUI(scene); 298 | 299 | window.addEventListener('message', event => { 300 | const message = event.data; // The JSON data our extension sent 301 | switch (message.command) { 302 | case 'urdf': 303 | applyURDF(message.urdf); 304 | break; 305 | case 'previewFile': 306 | vscode.setState({previewFile: message.previewFile}); 307 | break; 308 | case 'colors': 309 | camera.radius = message.cameraRadius; 310 | scene.clearColor = BABYLON.Color4.FromHexString(message.backgroundColor); 311 | let gm = ground.material as Materials.GridMaterial; 312 | gm.lineColor = BABYLON.Color3.FromHexString(message.gridLineColor); 313 | gm.mainColor = BABYLON.Color3.FromHexString(message.gridMainColor); 314 | gm.minorUnitVisibility = parseFloat(message.gridMinorOpacity); 315 | 316 | readyToRender = true; 317 | break; 318 | } 319 | }); 320 | 321 | engine.runRenderLoop(function () { 322 | if (scene != undefined && readyToRender) { 323 | scene.render(); 324 | } 325 | }); 326 | 327 | engine.resize(); 328 | 329 | window.addEventListener("resize", function () { 330 | engine.resize(); 331 | }); 332 | 333 | 334 | } 335 | 336 | // Just like a regular webpage we need to wait for the webview 337 | // DOM to load before we can reference any of the HTML elements 338 | // or toolkit components 339 | window.addEventListener("load", main); 340 | 341 | -------------------------------------------------------------------------------- /src/urdfPreview/previewManager.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from 'vscode'; 5 | import * as path from "path"; 6 | 7 | import * as extension from "../extension"; 8 | import * as telemetry from "../telemetry-helper"; 9 | import URDFPreview from './preview' 10 | 11 | export default class URDFPreviewManager implements vscode.WebviewPanelSerializer { 12 | public static readonly INSTANCE = new URDFPreviewManager(); 13 | 14 | private readonly _previews: URDFPreview[] = []; 15 | private _activePreview: URDFPreview | undefined = undefined; 16 | private _context: vscode.ExtensionContext; 17 | 18 | public setContext(context: vscode.ExtensionContext) 19 | { 20 | this._context = context; 21 | } 22 | 23 | public refresh() { 24 | for (const preview of this._previews) { 25 | preview.refresh(); 26 | } 27 | } 28 | 29 | public preview( 30 | resource: vscode.Uri 31 | ): void { 32 | const reporter = telemetry.getReporter(); 33 | reporter.sendTelemetryCommand(extension.Commands.PreviewURDF); 34 | if (URDFPreviewManager.handlesUri(resource)) { 35 | let preview = this.getExistingPreview(resource); 36 | if (preview) { 37 | preview.reveal(); 38 | } else { 39 | preview = this.createNewPreview(this._context, resource); 40 | } 41 | 42 | preview.update(resource); 43 | } 44 | } 45 | 46 | public get activePreviewResource() { 47 | return this._activePreview && this._activePreview.resource; 48 | } 49 | 50 | public async deserializeWebviewPanel( 51 | webview: vscode.WebviewPanel, 52 | state: any 53 | ): Promise { 54 | if (state) { 55 | const preview = await URDFPreview.revive( 56 | webview, 57 | this._context, 58 | state); 59 | 60 | this.registerPreview(preview); 61 | } 62 | } 63 | 64 | private getExistingPreview( 65 | resource: vscode.Uri 66 | ): URDFPreview | undefined { 67 | return this._previews.find(preview => 68 | preview.matchesResource(resource)); 69 | } 70 | 71 | private createNewPreview( 72 | context: vscode.ExtensionContext, 73 | resource: vscode.Uri 74 | ): URDFPreview { 75 | const preview = URDFPreview.create( 76 | context, 77 | resource); 78 | 79 | this._activePreview = preview; 80 | return this.registerPreview(preview); 81 | } 82 | 83 | private registerPreview( 84 | preview: URDFPreview 85 | ): URDFPreview { 86 | this._previews.push(preview); 87 | 88 | preview.onDispose(() => { 89 | const existing = this._previews.indexOf(preview); 90 | if (existing === -1) { 91 | return; 92 | } 93 | 94 | this._previews.splice(existing, 1); 95 | if (this._activePreview === preview) { 96 | this._activePreview = undefined; 97 | } 98 | }); 99 | 100 | preview.onDidChangeViewState(({ webviewPanel }) => { 101 | this._activePreview = webviewPanel.active ? preview : undefined; 102 | }); 103 | 104 | return preview; 105 | } 106 | 107 | private static handlesUri( 108 | uri: vscode.Uri 109 | ) : boolean { 110 | 111 | let ext = path.extname(uri.fsPath); 112 | if (ext == ".xacro" || 113 | ext == ".urdf") { 114 | return true; 115 | } 116 | 117 | return false; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/vscode-utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | import * as vscode from "vscode"; 5 | 6 | export interface IPackageInfo { 7 | name: string; 8 | version: string; 9 | aiKey: string; 10 | } 11 | 12 | export function getPackageInfo(extensionId: string): IPackageInfo { 13 | const extension = vscode.extensions.getExtension(extensionId); 14 | const metadata = extension.packageJSON; 15 | if (metadata && ("name" in metadata) && ("version" in metadata) && ("aiKey" in metadata)) { 16 | return { 17 | name: metadata.name, 18 | version: metadata.version, 19 | aiKey: metadata.aiKey, 20 | }; 21 | } 22 | return undefined; 23 | } 24 | 25 | export function getExtensionConfiguration(): vscode.WorkspaceConfiguration { 26 | const rosConfigurationName: string = "ros"; 27 | return vscode.workspace.getConfiguration(rosConfigurationName); 28 | } 29 | 30 | export function createOutputChannel(): vscode.OutputChannel { 31 | return vscode.window.createOutputChannel("ROS"); 32 | } 33 | -------------------------------------------------------------------------------- /templates/preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 144 | 145 | 146 | 147 |
148 | 149 | 150 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution":"Node", 5 | "target": "es6", 6 | "outDir": "out", 7 | "lib": [ 8 | "dom", 9 | "es6" 10 | ], 11 | "sourceMap": true, 12 | "rootDir": ".", 13 | "noImplicitAny": false 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | ".vscode-test" 18 | ] 19 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest" 3 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 8 | 9 | /** @type WebpackConfig */ 10 | const baseConfig = { 11 | mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 12 | externals: { 13 | vscode: "commonjs vscode", // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 14 | // modules added here also need to be added in the .vscodeignore file 15 | }, 16 | resolve: { 17 | extensions: [".ts", ".tsx", ".js"], 18 | }, 19 | devtool: "source-map", 20 | infrastructureLogging: { 21 | level: "log", // enables logging required for problem matchers 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.tsx?$/, 27 | exclude: /node_modules/, 28 | use: [{ loader: "ts-loader" }], 29 | }, 30 | ], 31 | } 32 | }; 33 | 34 | // Config for extension source code (to be run in a Node-based context) 35 | /** @type WebpackConfig */ 36 | const extensionConfig = { 37 | ...baseConfig, 38 | target: "node", 39 | entry: "./src/extension.ts", 40 | externals: { 41 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 42 | 'applicationinsights-native-metrics': 'commonjs applicationinsights-native-metrics' // ignored because we don't ship native module 43 | }, 44 | output: { 45 | path: path.resolve(__dirname, "dist"), 46 | filename: "extension.js", 47 | libraryTarget: "commonjs2", 48 | devtoolModuleFilenameTemplate: '../[resource-path]' 49 | }, 50 | }; 51 | 52 | /** @type WebpackConfig */ 53 | const ros1_webview_config = { 54 | ...baseConfig, 55 | target: ["web", "es2022"], 56 | entry: "./src/ros/ros1/webview/ros1_webview_main.ts", 57 | experiments: { outputModule: true, topLevelAwait: true }, 58 | resolve: { 59 | extensions: [".ts", ".tsx", ".js"], 60 | }, 61 | output: { 62 | path: path.resolve(__dirname, "dist"), 63 | filename: "ros1_webview_main.js", 64 | libraryTarget: "module", 65 | chunkFormat: "module", 66 | }, 67 | }; 68 | 69 | /** @type WebpackConfig */ 70 | const ros2_webview_config = { 71 | ...baseConfig, 72 | target: ["web", "es2022"], 73 | entry: "./src/ros/ros2/webview/ros2_webview_main.ts", 74 | experiments: { outputModule: true, topLevelAwait: true }, 75 | resolve: { 76 | extensions: [".ts", ".tsx", ".js"], 77 | }, 78 | output: { 79 | path: path.resolve(__dirname, "dist"), 80 | filename: "ros2_webview_main.js", 81 | libraryTarget: "module", 82 | chunkFormat: "module", 83 | }, 84 | }; 85 | 86 | // Config for webview source code (to be run in a web-based context) 87 | /** @type WebpackConfig */ 88 | const webviewURDFConfig = { 89 | ...baseConfig, 90 | target: ["web", "es2022"], 91 | entry: "./src/urdfPreview/URDFPreviewPanel.ts", 92 | experiments: { outputModule: true, topLevelAwait: true }, 93 | output: { 94 | path: path.resolve(__dirname, "dist"), 95 | filename: "webview.js", 96 | libraryTarget: "module", 97 | chunkFormat: "module", 98 | }, 99 | }; 100 | 101 | 102 | module.exports = [extensionConfig, ros1_webview_config, ros2_webview_config, webviewURDFConfig]; 103 | --------------------------------------------------------------------------------