├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml ├── semantic.yml └── workflows │ ├── build.yml │ └── semantic.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE.txt ├── README.md ├── codecov.yml ├── images ├── demo.gif └── logo.png ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── TerminalHelper.scpt ├── breakpoints.ts ├── cloud.ts ├── dbgp.ts ├── envfile.ts ├── extension.ts ├── ignore.ts ├── logpoint.ts ├── paths.ts ├── phpDebug.ts ├── proxyConnect.ts ├── terminal.ts ├── terminateProcess.sh ├── test │ ├── adapter.ts │ ├── cloud.ts │ ├── dbgp.ts │ ├── envfile.ts │ ├── ignore.ts │ ├── logpoint.ts │ ├── paths.ts │ └── proxy.ts ├── varExport.ts └── xdebugConnection.ts ├── testproject ├── .vscode │ └── launch.json ├── envfile ├── error.php ├── folder with spaces │ └── file with spaces.php ├── function.php ├── hello_world.php ├── hit.php ├── ignore_exception.php ├── output.php ├── stack.php └── variables.php └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | insert_spaces = true 4 | indent_size = 4 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = lf 8 | charset = utf-8 9 | 10 | [{*.json,*.yml,.prettierrc,*.md}] 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | "env": { 16 | "browser": true, 17 | "es6": true, 18 | "node": true 19 | }, 20 | "parser": "@typescript-eslint/parser", 21 | "parserOptions": { 22 | "project": "tsconfig.json", 23 | "sourceType": "module" 24 | }, 25 | "extends": [ 26 | 'eslint:recommended', 27 | //'plugin:jsdoc/recommended', 28 | 'plugin:@typescript-eslint/eslint-recommended', 29 | 'plugin:@typescript-eslint/recommended', 30 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 31 | 'prettier', 32 | /* 33 | 'plugin:react/recommended', 34 | 'plugin:@typescript-eslint/eslint-recommended', 35 | 'plugin:@typescript-eslint/recommended', 36 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 37 | 'prettier', 38 | 'prettier/@typescript-eslint', 39 | 'plugin:import/errors', 40 | 'plugin:import/warnings', 41 | 'plugin:import/typescript', 42 | 'plugin:unicorn/recommended', 43 | 'plugin:jest-dom/recommended', 44 | 'plugin:jsx-a11y/recommended', 45 | */ 46 | ], 47 | "plugins": [ 48 | "eslint-plugin-jsdoc", 49 | "@typescript-eslint" 50 | ], 51 | "root": true, 52 | "rules": { 53 | '@typescript-eslint/no-unused-vars': [ 54 | 'warn', 55 | { 56 | varsIgnorePattern: '.*', // TS already enforces this 57 | args: 'none', 58 | ignoreRestSiblings: true, 59 | }, 60 | ], 61 | 'no-unused-vars': 'off', 62 | 'unused-imports/no-unused-vars': 'off', 63 | 'no-constant-condition': ['error', { checkLoops: false }], 64 | 'no-dupe-class-members': 'off', 65 | 'no-redeclare': 'off', 66 | 'prefer-rest-params': 'off', 67 | '@typescript-eslint/no-inferrable-types': ['error', { ignoreParameters: true }], 68 | '@typescript-eslint/no-non-null-assertion': 'off', 69 | '@typescript-eslint/no-unsafe-assignment': 'off', 70 | /* 71 | "@typescript-eslint/indent": "off", 72 | "@typescript-eslint/member-delimiter-style": [ 73 | "off", 74 | { 75 | "multiline": { 76 | "delimiter": "none", 77 | "requireLast": true 78 | }, 79 | "singleline": { 80 | "delimiter": "semi", 81 | "requireLast": false 82 | } 83 | } 84 | ], 85 | '@typescript-eslint/naming-convention': [ 86 | 'off', 87 | { 88 | // Properties and destructured variables often can't be controlled by us if the API is external. 89 | // Event logging, `__typename` etc don't follow conventions enforceable here. 90 | // We also need to allow implementing external interface methods, e.g. UNSAFE_componentWillReceiveProps(). 91 | selector: 'default', 92 | format: null, 93 | }, 94 | { 95 | // Helps e.g. Go engineers who are used to lowercase unexported types. 96 | selector: 'typeLike', 97 | format: ['PascalCase'], 98 | leadingUnderscore: 'allow', 99 | trailingUnderscore: 'allow', 100 | }, 101 | ], 102 | "@typescript-eslint/no-empty-function": "error", 103 | "@typescript-eslint/no-unused-expressions": "error", 104 | "@typescript-eslint/prefer-namespace-keyword": "error", 105 | "@typescript-eslint/quotes": "off", 106 | "@typescript-eslint/semi": [ 107 | "off", 108 | null 109 | ], 110 | "@typescript-eslint/type-annotation-spacing": "off", 111 | "arrow-parens": [ 112 | "off", 113 | "always" 114 | ], 115 | "brace-style": [ 116 | "off", 117 | "off" 118 | ], 119 | "comma-dangle": "off", 120 | "curly": "error", 121 | "eol-last": "off", 122 | "eqeqeq": [ 123 | "error", 124 | "smart" 125 | ], 126 | "id-denylist": [ 127 | "error", 128 | "any", 129 | "Number", 130 | "number", 131 | "String", 132 | "string", 133 | "Boolean", 134 | "boolean", 135 | "Undefined", 136 | "undefined" 137 | ], 138 | "id-match": "error", 139 | "indent": "off", 140 | "jsdoc/check-alignment": "error", 141 | "jsdoc/check-indentation": "error", 142 | "jsdoc/newline-after-description": "error", 143 | "linebreak-style": "off", 144 | "max-len": "off", 145 | "new-parens": "off", 146 | "newline-per-chained-call": "off", 147 | "no-empty": "error", 148 | "no-empty-function": "off", 149 | "no-eval": "error", 150 | "no-extra-semi": "off", 151 | "no-irregular-whitespace": "off", 152 | "no-multiple-empty-lines": "off", 153 | "no-redeclare": "error", 154 | "no-trailing-spaces": "off", 155 | "no-underscore-dangle": "off", 156 | "no-unused-expressions": "off", 157 | "no-var": "error", 158 | "one-var": [ 159 | "error", 160 | "never" 161 | ], 162 | "padded-blocks": [ 163 | "off", 164 | { 165 | "blocks": "never" 166 | }, 167 | { 168 | "allowSingleLineBlocks": true 169 | } 170 | ], 171 | "quote-props": "off", 172 | "quotes": "off", 173 | "semi": "off", 174 | "space-before-function-paren": "off", 175 | "space-in-parens": [ 176 | "off", 177 | "never" 178 | ], 179 | "spaced-comment": [ 180 | "error", 181 | "always", 182 | { 183 | "markers": [ 184 | "/" 185 | ] 186 | } 187 | ] 188 | */ 189 | } 190 | }; 191 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [zobo] 2 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | allowMergeCommits: true 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | env: 6 | FORCE_COLOR: 3 7 | 8 | jobs: 9 | test: 10 | if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | include: 15 | # Latest versions 16 | - xdebug: xdebug-3.4.2 # renovate:keep-up-to-date 17 | php: '8.2' # renovate:keep-up-to-date 18 | os: ubuntu-22.04 19 | - xdebug: xdebug-3.4.2 # renovate:keep-up-to-date 20 | php: '8.1' # renovate:keep-up-to-date 21 | os: windows-2022 22 | - xdebug: xdebug-3.4.2 # renovate:keep-up-to-date 23 | php: '8.2' # renovate:keep-up-to-date 24 | os: macos-14 25 | # Old versions 26 | - xdebug: xdebug-2.9.8 27 | php: '7.4' 28 | os: ubuntu-22.04 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: '20.18.2' # renovate:keep-up-to-date 35 | - name: Install npm dependencies 36 | run: npm ci 37 | - name: Prettier 38 | run: npm run prettier-check 39 | - name: Lint 40 | run: npm run eslint 41 | - name: Build VS Code extension 42 | run: npm run build 43 | - name: Setup PHP 44 | uses: shivammathur/setup-php@v2 45 | with: 46 | php-version: ${{ matrix.php }} 47 | extensions: ${{ matrix.xdebug }} 48 | # Top: Xdebug v3 49 | # Bottom: Xdebug v2 50 | ini-values: >- 51 | xdebug.mode = debug, 52 | xdebug.start_with_request = yes, 53 | 54 | xdebug.remote_enable = 1, 55 | xdebug.remote_autostart = 1, 56 | xdebug.remote_port = 9003, 57 | xdebug.remote_log = /tmp/xdebug.log 58 | - name: Run tests 59 | run: npm run cover 60 | - name: Generate coverage report 61 | run: ./node_modules/.bin/nyc report --reporter=json 62 | - name: Upload coverage to Codecov 63 | uses: codecov/codecov-action@v3 64 | release: 65 | runs-on: ubuntu-22.04 66 | needs: test 67 | if: github.repository_owner == 'xdebug' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' 68 | steps: 69 | - uses: actions/checkout@v4 70 | - name: Setup Node.js 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version: '20.18.2' # renovate:keep-up-to-date 74 | - name: Install npm dependencies 75 | run: npm ci 76 | - name: Build VS Code extension 77 | run: npm run build 78 | - name: Release 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 82 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 83 | run: npm run semantic-release 84 | -------------------------------------------------------------------------------- /.github/workflows/semantic.yml: -------------------------------------------------------------------------------- 1 | name: 'Semantic Pull Request' 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | # pull_request: 10 | # types: 11 | # - opened 12 | # - edited 13 | # - synchronize 14 | 15 | jobs: 16 | main: 17 | name: Validate PR Title 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: semantic-pull-request 21 | uses: amannn/action-semantic-pull-request@v4 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | validateSingleCommit: false 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | out/ 4 | coverage/ 5 | .nyc_output/ 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | out/ 4 | package-lock.json 5 | package.json 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "trailingComma": "es5", 6 | "bracketSpacing": true, 7 | "singleQuote": true, 8 | "printWidth": 120, 9 | "endOfLine": "lf", 10 | "proseWrap": "preserve", 11 | "arrowParens": "avoid", 12 | "overrides": [ 13 | { 14 | "files": "{*.js?(on),*.yml,.*.yml,.prettierrc,*.md}", 15 | "options": { 16 | "tabWidth": 2 17 | } 18 | }, 19 | { 20 | "files": ".prettierrc", 21 | "options": { 22 | "parser": "json" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "EditorConfig.editorconfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug adapter", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/src/phpDebug.ts", 9 | "cwd": "${workspaceRoot}", 10 | "args": ["--server=4711"], 11 | "env": { 12 | "NODE_ENV": "development" 13 | }, 14 | "sourceMaps": true, 15 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 16 | }, 17 | { 18 | "name": "Launch Extension", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 25 | }, 26 | { 27 | "name": "Mocha", 28 | "type": "node", 29 | "request": "launch", 30 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 31 | "args": ["out/test", "--no-timeouts", "--colors"], 32 | "cwd": "${workspaceRoot}", 33 | "sourceMaps": true, 34 | "env": { 35 | "VSCODE_DEBUG_PORT": "4711" 36 | }, 37 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 38 | } 39 | ], 40 | "compounds": [ 41 | { 42 | "name": "PHP Debug", 43 | "stopAll": true, 44 | "configurations": ["Debug adapter", "Launch Extension"], 45 | "presentation": { 46 | "group": "0_php", 47 | "order": 1 48 | } 49 | }, 50 | { 51 | "name": "Unit tests", 52 | "stopAll": true, 53 | "configurations": ["Debug adapter", "Mocha"], 54 | "presentation": { 55 | "group": "0_php", 56 | "order": 2 57 | } 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "out": true // set this to false to include "out" folder in search results 5 | }, 6 | "javascript.validate.enable": false, 7 | "editor.insertSpaces": true, 8 | "editor.tabSize": 4, 9 | "typescript.tsdk": "./node_modules/typescript/lib" 10 | } 11 | -------------------------------------------------------------------------------- /.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 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$tsc-watch"], 10 | "isBackground": true, 11 | "label": "npm: watch", 12 | "detail": "tsc -w -p .", 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "label": "npm: build", 20 | "type": "npm", 21 | "script": "build", 22 | "problemMatcher": ["$tsc"], 23 | "isBackground": false 24 | }, 25 | { 26 | "type": "npm", 27 | "script": "lint", 28 | "problemMatcher": ["$eslint-stylish"], 29 | "label": "npm: lint", 30 | "detail": "npm run eslint && npm run prettier" 31 | }, 32 | { 33 | "type": "npm", 34 | "script": "test", 35 | "group": "test", 36 | "problemMatcher": [], 37 | "label": "npm: test", 38 | "detail": "mocha", 39 | "options": { 40 | "env": { 41 | "VSCODE_DEBUG_PORT": "4711" 42 | } 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .nyc_output/** 2 | .vscode/** 3 | coverage/** 4 | images/** 5 | !images/logo.png 6 | out/test/** 7 | src/** 8 | testproject/** 9 | .editorconfig 10 | .gitignore 11 | .npmrc 12 | .prettierignore 13 | .prettierrc 14 | .travis.yml 15 | *.log 16 | *.vsix 17 | **/*.js.map 18 | appveyor-install.ps1 19 | appveyor.yml 20 | gulpfile.js 21 | ISSUE_TEMPLATE.md 22 | package-lock.json 23 | travis-php.ini 24 | tsconfig.json 25 | .eslintrc.js 26 | renovate.json 27 | .github 28 | codecov.yml 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [1.36.1] 8 | 9 | - Improve handling of watch for static properties 10 | 11 | ## [1.36.0] 12 | 13 | - Implement copy to clipboard in var_export format 14 | - Improve watch eval to cache result or use property 15 | - Update docs 16 | 17 | ## [1.35.0] 18 | 19 | - Support for DBGp stream command 20 | - Avoid conflict with full screen F11 shortcut 21 | - Improve existing unix socket handling 22 | - Improve close socket handling 23 | 24 | ## [1.34.0] 25 | 26 | - Partial support for virtual workspaces 27 | 28 | ## [1.33.1] 29 | 30 | - Fix editor title run/debug button. 31 | 32 | ## [1.33.0] 33 | 34 | - Add skipEntryPaths to immediately detach a debug session depending on entry path. 35 | - Remove EvaluatableExpression Provider. 36 | 37 | ## [1.32.1] 38 | 39 | - Fix logging of cloud connection. 40 | - Fix ignore exceptions patterns and namespaces. 41 | 42 | ## [1.32.0] 43 | 44 | - New launch setting ignoreExceptions. 45 | 46 | ## [1.31.1] 47 | 48 | - Fix relative paths and path mappings support. 49 | 50 | ## [1.31.0] 51 | 52 | - Allow more flexible path mappings in url format. 53 | 54 | ## [1.30.0] 55 | 56 | - Add skipFiles launch setting to skip over specified file patterns. 57 | 58 | ## [1.29.1] 59 | 60 | - Fix for env configuration check that sometimes causes an error. 61 | 62 | ## [1.29.0] 63 | 64 | - Xdebug Cloud support. 65 | 66 | ## [1.28.0] 67 | 68 | - Support for envFile. 69 | - Migrated from tslint to eslint. 70 | 71 | ## [1.27.0] 72 | 73 | - Variable paging with VSCode indexedVariables. 74 | - Enable return value stepping with breakpoint_include_return_value. 75 | 76 | ## [1.26.1] 77 | 78 | - Fixed typo in error message for unexpected env. Extended error message with more detail. 79 | 80 | ## [1.26.0] 81 | 82 | - Support for Unix Domain sockets #777 83 | - Improve ExitedEvent notification #763 84 | - Improve Debug Console (Eval) handling of nested vars #764 85 | - Fixed missing TerminalHelper script #762 86 | 87 | ## [1.25.0] 88 | 89 | - Implement delayed stack loading with startFrame and levels argument to StackTrace Request 90 | 91 | ## [1.24.3] 92 | 93 | - Fix for broken property traversal #755 94 | 95 | ## [1.24.2] 96 | 97 | - Additional fix for extended root property in eval #751 98 | 99 | ## [1.24.1] 100 | 101 | - Fix for extended root property #751 102 | 103 | ## [1.24.0] 104 | 105 | - F10/F11 start debugging with stop on entry. 106 | 107 | ## [1.23.0] 108 | 109 | - When `env` is specified in launch configuration it will be merged the process environment. 110 | - Set variable support. 111 | - Improved hover support. 112 | - Update publisher id. 113 | 114 | ## [1.22.0] 115 | 116 | ### Added 117 | 118 | - DBGp Proxy support. 119 | - `php.debug.ideKey` setting to set the Proxy IDE key globally. 120 | 121 | ### Changed 122 | 123 | - Renamed `php.executablePath` setting to `php.debug.executablePath` and do not fallback to `php.validate.executablePath`. 124 | - Untrusted workspace settings. 125 | - Default protocol encoding changed to utf-8. 126 | 127 | ## [1.21.1] 128 | 129 | ### Fixed 130 | 131 | - Auto configure runtimeExecutable when only runtimeArgs are used (built-in web server). 132 | - Improve handling of broken clients on failed initPacket. 133 | 134 | ## [1.21.0] 135 | 136 | ### Added 137 | 138 | - Support for maxConnections limiting how many parallel connections the debug adapter allows. 139 | 140 | ## [1.20.0] 141 | 142 | ### Added 143 | 144 | - Support no-folder debugging in (purple) VS Code. 145 | 146 | ## [1.19.0] 147 | 148 | ### Added 149 | 150 | - Support for PHP 8.1 facets 151 | - Support for Xdebug 3.1 xdebug_notify() 152 | 153 | ## [1.18.0] 154 | 155 | - Added hit count breakpoint condition. 156 | 157 | ## [1.17.0] 158 | 159 | ### Added 160 | 161 | - Added logpoint support. 162 | 163 | ## [1.16.3] 164 | 165 | ### Fixed 166 | 167 | - Fixed semver dependency error. 168 | 169 | ## [1.16.2] 170 | 171 | ### Fixed 172 | 173 | - Fixed breakpoint and launch initialization order. 174 | - Optimize feature negotiation for known Xdebug version. 175 | 176 | ## [1.16.1] 177 | 178 | ### Fixed 179 | 180 | - Do not request all breakpoints on every new Xdebug connection. Use internal BreakpointManager state. 181 | - Show breakpoints as verified when there are no connections. 182 | 183 | ## [1.16.0] 184 | 185 | ### Added 186 | 187 | - Option to start PHP built-in web server without router script. 188 | - Extended logging with DBGp packets. 189 | - Extended properties support. Always enable extended properties so fields are decoded in UTF-8. 190 | 191 | ### Changed 192 | 193 | - Switched to Xdebug 3 default port 9003. 194 | - Changed default exception breakpoint settings to all off. 195 | 196 | ### Fixed 197 | 198 | - Internal Source Reference for virtual source files fixed - when stepping into eval() 199 | 200 | ## [1.15.1] 201 | 202 | ### Changed 203 | 204 | - Defined under packages.json this extension should be preferred for PHP debugging. 205 | 206 | ## [1.15.0] 207 | 208 | ### Added 209 | 210 | - Support for terminateDebuggee option letting the user choose to keep the debuggee running. Press Alt when hovering over stop action. 211 | - Handle breakpoints in a async manner. 212 | 213 | ### Changed 214 | 215 | - Do not display error dialog on failed eval 216 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | PHP version: 2 | Xdebug version: 3 | VS Code extension version: 4 | 5 | Your launch.json: 6 | Xdebug php.ini config: 7 | 8 | Xdebug logfile (from setting `xdebug.log` in php.ini): 9 | VS Code extension logfile (from setting `"log": true` in launch.json): 10 | 11 | Code snippet to reproduce: 12 | 13 | ```php 14 | 15 | ``` 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2018 Felix Frederick Becker 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Debug Adapter for Visual Studio Code 2 | 3 | [![vs marketplace](https://img.shields.io/vscode-marketplace/v/xdebug.php-debug.svg?label=vs%20marketplace)](https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug) [![downloads](https://img.shields.io/vscode-marketplace/d/xdebug.php-debug.svg)](https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug) [![rating](https://img.shields.io/vscode-marketplace/r/xdebug.php-debug.svg)](https://marketplace.visualstudio.com/items?itemName=xdebug.php-debug) [![build status](https://img.shields.io/github/workflow/status/xdebug/vscode-php-debug/build/main?logo=github)](https://github.com/xdebug/vscode-php-debug/actions?query=branch%3Amain) [![codecov](https://codecov.io/gh/xdebug/vscode-php-debug/branch/main/graph/badge.svg)](https://codecov.io/gh/xdebug/vscode-php-debug) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 4 | 5 | ![Demo GIF](images/demo.gif) 6 | 7 | ### Sponsor PHP Debug Adapter for Visual Studio Code 8 | 9 | If you find this extension useful, if it helps you solve your problems and if you appreciate the support given here, consider sponsoring our work. 10 | 11 | ## Installation 12 | 13 | Install the extension: Press `F1`, type `ext install php-debug`. 14 | 15 | This extension is a debug adapter between VS Code and [Xdebug](https://xdebug.org/) by Derick Rethans. Xdebug is a PHP extension (a `.so` file on Linux and a `.dll` on Windows) that needs to be installed on your server. 16 | 17 | 1. [Install Xdebug](https://xdebug.org/docs/install) 18 | **_I highly recommend you make a simple `test.php` file, put a `phpinfo();` statement in there, then copy the output and paste it into the [Xdebug installation wizard](https://xdebug.org/wizard.php). It will analyze it and give you tailored installation instructions for your environment._** In short: 19 | 20 | - On Windows: [Download](https://xdebug.org/download.php) the appropriate precompiled DLL for your PHP version, architecture (64/32 Bit), thread safety (TS/NTS) and Visual Studio compiler version and place it in your PHP extension folder. 21 | - On Linux: Either download the source code as a tarball or [clone it with git](https://xdebug.org/docs/install#source), then [compile it](https://xdebug.org/docs/install#compile). Or see if your distribution already offers prebuilt packages. 22 | 23 | 2. [Configure PHP to use Xdebug](https://xdebug.org/docs/install#configure-php) by adding `zend_extension=path/to/xdebug` to your php.ini. The path of your php.ini is shown in your `phpinfo()` output under "Loaded Configuration File". 24 | 25 | 3. Enable remote debugging in your `php.ini`: 26 | 27 | For Xdebug v3.x.x: 28 | 29 | ```ini 30 | xdebug.mode = debug 31 | xdebug.start_with_request = yes 32 | ``` 33 | 34 | For Xdebug v2.x.x: 35 | 36 | ```ini 37 | xdebug.remote_enable = 1 38 | xdebug.remote_autostart = 1 39 | xdebug.remote_port = 9000 40 | ``` 41 | 42 | There are other ways to tell Xdebug to connect to a remote debugger, like cookies, query parameters or browser extensions. I recommend `remote_autostart` (Xdebug v2)/`start_with_request` (Xdebug v3) because it "just works". There are also a variety of other options, like the port, please see the [Xdebug documentation on remote debugging](https://xdebug.org/docs/remote#starting) for more information. Please note that the default Xdebug port changed between Xdebug v2 to v3 from 9000 to 9003. 43 | 44 | 4. If you are doing web development, don't forget to restart your webserver to reload the settings. 45 | 46 | 5. Verify your installation by checking your `phpinfo()` output for an Xdebug section. 47 | 48 | ### VS Code Configuration 49 | 50 | In your project, go to the debugger and hit the little gear icon and choose _PHP_. A new launch configuration will be created for you with three configurations: 51 | 52 | - **Listen for Xdebug** 53 | This setting will simply start listening on the specified port (by default 9003) for Xdebug. If you configured Xdebug like recommended above, every time you make a request with a browser to your webserver or launch a CLI script Xdebug will connect and you can stop on breakpoints, exceptions etc. 54 | - **Launch currently open script** 55 | This setting is an example of CLI debugging. It will launch the currently opened script as a CLI, show all stdout/stderr output in the debug console and end the debug session once the script exits. 56 | - **Launch Built-in web server** 57 | This configuration starts the PHP built-in web server on a random port and opens the browser with the `serverReadyAction` directive. The port is random (localhost:0) but can be changed to a desired fixed port (ex: localhost:8080). If a router script is needed, add it with `program` directive. Additional PHP/Xdebug directives trigger debugging on every page load. 58 | 59 | There are also configurations for Xdebug v2 (Legacy) installations. 60 | 61 | More general information on debugging with VS Code can be found on https://code.visualstudio.com/docs/editor/debugging. 62 | 63 | > _Note:_ You can even debug a script without `launch.json`. If no folder is open, and the VS Code status bar is purple, pressing `F5` will start the open script with Xdebug3 specific parameters. If the php executable is not in path, you can provide it with the setting `php.debug.executablePath`. For debugging to work, Xdebug must still be correctly installed. 64 | 65 | #### Supported launch.json settings: 66 | 67 | - `request`: Always `"launch"` 68 | - `hostname`: The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with `unix://`) or Windows Pipe (`\\?\pipe\name`) - cannot be combined with port 69 | - `port`: The port on which to listen for Xdebug (default: `9003`). If port is set to `0` a random port is chosen by the system and a placeholder `${port}` is replaced with the chosen port in `env` and `runtimeArgs`. 70 | - `stopOnEntry`: Whether to break at the beginning of the script (default: `false`) 71 | - `pathMappings`: A list of server paths mapping to the local source paths on your machine, see "Remote Host Debugging" below 72 | - `log`: Whether to log all communication between VS Code and the adapter to the debug console. See _Troubleshooting_ further down. 73 | - `ignore`: An optional array of glob patterns that errors should be ignored from (for example `**/vendor/**/*.php`) 74 | - `ignoreExceptions`: An optional array of exception class names that should be ignored (for example `BaseException`, `\NS1\Exception`, `\*\Exception` or `\**\Exception*`) 75 | - `skipFiles`: An array of glob patterns, to skip when debugging. Star patterns and negations are allowed, for example, `**/vendor/**` or `!**/vendor/my-module/**`. 76 | - `skipEntryPaths`: An array of glob patterns, to immediately detach from and ignore for debugging if the entry script matches (example `**/ajax.php`). 77 | - `maxConnections`: Accept only this number of parallel debugging sessions. Additional connections will be dropped and their execution will continue without debugging. 78 | - `proxy`: DBGp Proxy settings 79 | - `enable`: To enable proxy registration set to `true` (default is `false). 80 | - `host`: The address of the proxy. Supports host name, IP address, or Unix domain socket (default: 127.0.0.1). 81 | - `port`: The port where the adapter will register with the the proxy (default: `9001`). 82 | - `key`: A unique key that allows the proxy to match requests to your editor (default: `vsc`). The default is taken from VSCode settings `php.debug.idekey`. 83 | - `timeout`: The number of milliseconds to wait before giving up on the connection to proxy (default: `3000`). 84 | - `allowMultipleSessions`: If the proxy should forward multiple sessions/connections at the same time or not (default: `true`). 85 | - `xdebugSettings`: Allows you to override Xdebug's remote debugging settings to fine tuning Xdebug to your needs. For example, you can play with `max_children` and `max_depth` to change the max number of array and object children that are retrieved and the max depth in structures like arrays and objects. This can speed up the debugger on slow machines. 86 | For a full list of feature names that can be set please refer to the [Xdebug documentation](https://xdebug.org/docs-dbgp.php#feature-names). 87 | - `max_children`: max number of array or object children to initially retrieve 88 | - `max_data`: max amount of variable data to initially retrieve. 89 | - `max_depth`: maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE (there should be no need to change this as depth is retrieved incrementally, large value can cause IDE to hang). 90 | - `show_hidden`: This feature can get set by the IDE if it wants to have more detailed internal information on properties (eg. private members of classes, etc.) Zero means that hidden members are not shown to the IDE. 91 | - `breakpoint_include_return_value`: Determines whether to enable an additional "return from function" debugging step, allowing inspection of the return value when a function call returns. 92 | - `xdebugCloudToken`: Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection. 93 | - `stream`: Allows to influence DBGp streams. Xdebug only supports `stdout` see [DBGp stdout](https://xdebug.org/docs/dbgp#stdout-stderr) 94 | - `stdout`: Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect) 95 | 96 | Options specific to CLI debugging: 97 | 98 | - `program`: Path to the script that should be launched 99 | - `args`: Arguments passed to the script 100 | - `cwd`: The current working directory to use when launching the script 101 | - `runtimeExecutable`: Path to the PHP binary used for launching the script. By default the one on the PATH. 102 | - `runtimeArgs`: Additional arguments to pass to the PHP binary 103 | - `externalConsole`: Launches the script in an external console window instead of the debug console (default: `false`) 104 | - `env`: Environment variables to pass to the script 105 | - `envFile`: Optional path to a file containing environment variable definitions 106 | 107 | ## Features 108 | 109 | - Line breakpoints 110 | - Conditional breakpoints 111 | - Hit count breakpoints: supports the conditions like `>=n`, `==n` and `%n` 112 | - Function breakpoints 113 | - Step over, step in, step out 114 | - Break on entry 115 | - Start with Stop on entry (F10/F11) 116 | - Breaking on uncaught exceptions and errors / warnings / notices 117 | - Multiple, parallel requests 118 | - Stack traces, scope variables, superglobals, user defined constants 119 | - Arrays & objects (including classname, private and static properties) 120 | - Debug console 121 | - Watches 122 | - Set variables 123 | - Run as CLI 124 | - Run without debugging 125 | - DBGp Proxy registration and unregistration support 126 | - Xdebug Cloud support 127 | 128 | ## Remote Host Debugging 129 | 130 | To debug a running application on a remote host, you need to tell Xdebug to connect to a different IP than `localhost`. This can either be done by setting [`xdebug.client_host`](https://xdebug.org/docs/step_debug#client_host) to your IP or by setting [`xdebug.discover_client_host = 1`](https://xdebug.org/docs/all_settings#discover_client_host) to make Xdebug always connect back to the machine who did the web request. The latter is the only setting that supports multiple users debugging the same server and "just works" for web projects. Again, please see the [Xdebug documentation](https://xdebug.org/docs/remote#communcation) on the subject for more information. 131 | 132 | To make VS Code map the files on the server to the right files on your local machine, you have to set the `pathMappings` settings in your launch.json. Example: 133 | 134 | ```json 135 | // server -> local 136 | "pathMappings": { 137 | "/var/www/html": "${workspaceFolder}/www", 138 | "/app": "${workspaceFolder}/app" 139 | } 140 | ``` 141 | 142 | Please also note that setting any of the CLI debugging options will not work with remote host debugging, because the script is always launched locally. If you want to debug a CLI script on a remote host, you need to launch it manually from the command line. 143 | 144 | ## Proxy support 145 | 146 | The debugger can register itself to a DBGp proxy with a IDE Key. The proxy will then forward to the IDE only those DBGp sessions that have this specified IDE key. This is helpful in a multiuser environment where developers cannot use the same DBGp port at the same time. Careful setup is needed that requests to the web server contain the matching IDE key. 147 | 148 | The official implementation of the [dbgpProxy](https://xdebug.org/docs/dbgpProxy). 149 | 150 | A _Xdebug helper_ browser extension is also recommended. There the request side IDE key can be easily configured. 151 | 152 | ## Troubleshooting 153 | 154 | - Ask a question on [StackOverflow](https://stackoverflow.com/) 155 | - If you think you found a bug, [open an issue](https://github.com/xdebug/vscode-php-debug/issues) 156 | - Make sure you have the latest version of this extension and Xdebug installed 157 | - Try out a simple PHP file to recreate the issue, for example from the [testproject](https://github.com/xdebug/vscode-php-debug/tree/main/testproject) 158 | - Set `"log": true` in your launch.json and observe Debug Console panel 159 | - In your php.ini, set [`xdebug.log = /path/to/logfile`](https://xdebug.org/docs/step_debug#troubleshoot) 160 | (make sure your webserver has write permissions to the file) 161 | - Reach out on Twitter [@damjancvetko](https://twitter.com/damjancvetko) 162 | 163 | ## Contributing 164 | 165 | To hack on this adapter, clone the repository and open it in VS Code. You need NodeJS with NPM installed and in your PATH. Also a recent PHP and Xdebug should be installed and in your PATH. 166 | 167 | 1. Install NPM packages by either running `npm install` on command line in the project directory or selecting `Terminal / Run Task... / npm / npm: install` in VS Code menu. 168 | 2. Run the build/watch process either by running `npm run watch` on command line in the project directory or selecting `Terminal / Run Build Task...` in VS Code menu. 169 | 3. Start the debug adapter by opening the Run and Debug side bar, selecting `Debug adapter` configuration and clicking on the green Run arrow (or hitting `F5`). The compiled adapter will be running in "server mode" and listening on TCP port 4711. 170 | 4. Run a separate instance of VS Code, called "Extension Development Host" by running `code testproject --extensionDevelopmentPath=.` on command line in the project directory or selecting the `Launch Extension` Run and Debug configuration and pressing the green Run arrow. Another shortcut is to run `npm run start`. You can also run an Insiders build of VS Code for testing newer features. 171 | 5. In the "Extension Development Host" instance open `.vscode/launch.json` and uncomment the `debugServer` configuration line. Run your PHP debug session by selecting the desired configuration and hitting `F5`. Now you can debug the testproject like specified above and set breakpoints inside your first VS Code instance to step through the adapter code. 172 | 173 | More on testing extensions can be found on https://code.visualstudio.com/api/working-with-extensions/testing-extension. 174 | 175 | Tests are written with Mocha and can be run with `npm test` or from `Terminal / Run Task... / npm: test`. When you submit a PR the tests will be run in CI on Linux, macOS and Windows against multiple PHP and Xdebug versions. 176 | 177 | Before submitting a PR also run `npm run lint` or `Terminal / Run Tasks... / npm: lint`. 178 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - 'src/extension.ts' 3 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdebug/vscode-php-debug/8d2a50e0f1d43683b3ea306d5057562d030e02c1/images/demo.gif -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdebug/vscode-php-debug/8d2a50e0f1d43683b3ea306d5057562d030e02c1/images/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "php-debug", 3 | "displayName": "PHP Debug", 4 | "version": "0.0.0-development", 5 | "publisher": "xdebug", 6 | "license": "MIT", 7 | "description": "Debug support for PHP with Xdebug", 8 | "keywords": [ 9 | "php", 10 | "debug", 11 | "xdebug" 12 | ], 13 | "author": { 14 | "name": "Felix Becker", 15 | "email": "felix.b@outlook.com" 16 | }, 17 | "contributors": [ 18 | { 19 | "name": "Damjan Cvetko", 20 | "email": "damjan.cvetko@gmail.com" 21 | } 22 | ], 23 | "engines": { 24 | "vscode": "^1.66.1", 25 | "node": "^20.18.2" 26 | }, 27 | "sponsor": { 28 | "url": "https://github.com/sponsors/zobo" 29 | }, 30 | "icon": "images/logo.png", 31 | "galleryBanner": { 32 | "color": "#6682BA", 33 | "theme": "dark" 34 | }, 35 | "categories": [ 36 | "Debuggers" 37 | ], 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/xdebug/vscode-php-debug.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/xdebug/vscode-php-debug/issues" 44 | }, 45 | "dependencies": { 46 | "@vscode/debugadapter": "^1.68.0", 47 | "@vscode/debugprotocol": "^1.68.0", 48 | "@xmldom/xmldom": "^0.8.4", 49 | "buffer-crc32": "^0.2.13", 50 | "dotenv": "^16.0.3", 51 | "file-url": "^3.0.0", 52 | "iconv-lite": "^0.6.3", 53 | "minimatch": "^5.1.0", 54 | "moment": "^2.29.4", 55 | "relateurl": "^0.2.7", 56 | "semver": "^7.5.2", 57 | "string-replace-async": "^2.0.0", 58 | "which": "^2.0.2" 59 | }, 60 | "devDependencies": { 61 | "@commitlint/cli": "^17.3.0", 62 | "@commitlint/config-conventional": "^17.3.0", 63 | "@types/buffer-crc32": "^0.2.4", 64 | "@types/chai": "4.3.9", 65 | "@types/chai-as-promised": "^7.1.8", 66 | "@types/minimatch": "^5.1.2", 67 | "@types/mocha": "^10.0.0", 68 | "@types/node": "^16.11.27", 69 | "@types/relateurl": "^0.2.33", 70 | "@types/semver": "^7.3.13", 71 | "@types/vscode": "^1.66.0", 72 | "@types/which": "^2.0.2", 73 | "@typescript-eslint/eslint-plugin": "^5.44.0", 74 | "@typescript-eslint/parser": "^5.44.0", 75 | "@vscode/debugadapter-testsupport": "^1.68.0", 76 | "@vscode/vsce": "^3.2.2", 77 | "chai": "^4.3.10", 78 | "chai-as-promised": "^7.1.2", 79 | "copyfiles": "^2.4.1", 80 | "eslint": "^8.28.0", 81 | "eslint-config-prettier": "^8.10.0", 82 | "eslint-plugin-jsdoc": "^39.6.4", 83 | "husky": "^8.0.3", 84 | "mocha": "^10.1.0", 85 | "nyc": "^15.1.0", 86 | "prettier": "2.7.1", 87 | "semantic-release": "^19.0.5", 88 | "semantic-release-vsce": "^5.6.3", 89 | "typescript": "^4.9.3" 90 | }, 91 | "release": { 92 | "branches": [ 93 | "main" 94 | ], 95 | "verifyConditions": [ 96 | "semantic-release-vsce", 97 | "@semantic-release/github" 98 | ], 99 | "prepare": { 100 | "path": "semantic-release-vsce", 101 | "packageVsix": true 102 | }, 103 | "publish": [ 104 | "semantic-release-vsce", 105 | { 106 | "path": "@semantic-release/github", 107 | "assets": "*.vsix", 108 | "addReleases": "bottom" 109 | } 110 | ] 111 | }, 112 | "scripts": { 113 | "build": "npm run copyfiles && tsc -p .", 114 | "watch": "npm run copyfiles && tsc -w -p .", 115 | "copyfiles": "copyfiles -u 1 src/TerminalHelper.scpt src/terminateProcess.sh out", 116 | "start": "code testproject --extensionDevelopmentPath=.", 117 | "test": "mocha", 118 | "cover": "nyc mocha", 119 | "lint": "npm run eslint && npm run prettier", 120 | "eslint": "eslint \"src/**/*.ts\"", 121 | "prettier": "prettier \"**/{*.json,*.yml,.*.yml,*.ts,.prettierrc,*.md}\" --write --list-different", 122 | "prettier-check": "npm run prettier -- --write=false", 123 | "semantic-release": "semantic-release" 124 | }, 125 | "commitlint": { 126 | "extends": [ 127 | "@commitlint/config-conventional" 128 | ] 129 | }, 130 | "husky": { 131 | "hooks": { 132 | "commit-msg": "commitlint -E HUSKY_GIT_PARAM" 133 | } 134 | }, 135 | "mocha": { 136 | "spec": "./out/test", 137 | "timeout": 20000, 138 | "slow": 1000, 139 | "retries": 4 140 | }, 141 | "nyc": { 142 | "all": true, 143 | "include": [ 144 | "out/**/*.*", 145 | "src/**/*.*" 146 | ], 147 | "exclude": [ 148 | "out/test/**/*.*" 149 | ] 150 | }, 151 | "main": "./out/extension.js", 152 | "activationEvents": [ 153 | "onDebugResolve:php", 154 | "onCommand:extension.php-debug.startWithStopOnEntry", 155 | "onCommand:extension.php-debug.debugEditorContents", 156 | "onCommand:extension.php-debug.runEditorContents" 157 | ], 158 | "capabilities": { 159 | "untrustedWorkspaces": { 160 | "supported": "limited", 161 | "description": "%workspaceTrust%", 162 | "restrictedConfigurations": [ 163 | "php.debug.executablePath" 164 | ] 165 | }, 166 | "virtualWorkspaces": { 167 | "supported": "limited", 168 | "description": "In virtual workspaces, PHP process cannot be started, but can listen for incoming connections." 169 | } 170 | }, 171 | "contributes": { 172 | "breakpoints": [ 173 | { 174 | "language": "php" 175 | } 176 | ], 177 | "debuggers": [ 178 | { 179 | "type": "php", 180 | "languages": [ 181 | "php" 182 | ], 183 | "label": "PHP", 184 | "program": "./out/phpDebug.js", 185 | "runtime": "node", 186 | "configurationAttributes": { 187 | "launch": { 188 | "required": [], 189 | "properties": { 190 | "program": { 191 | "type": "string", 192 | "description": "Absolute path to the program." 193 | }, 194 | "stopOnEntry": { 195 | "type": "boolean", 196 | "description": "Automatically stop program after launch.", 197 | "default": false 198 | }, 199 | "externalConsole": { 200 | "type": "boolean", 201 | "description": "Launch debug target in external console.", 202 | "default": false 203 | }, 204 | "args": { 205 | "type": "array", 206 | "description": "Command line arguments passed to the program.", 207 | "items": { 208 | "type": "string" 209 | }, 210 | "default": [] 211 | }, 212 | "cwd": { 213 | "type": "string", 214 | "description": "Absolute path to the working directory of the program being debugged. Default is the current workspace.", 215 | "default": "${workspaceFolder}" 216 | }, 217 | "runtimeExecutable": { 218 | "type": "string", 219 | "description": "Absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.", 220 | "default": "php" 221 | }, 222 | "runtimeArgs": { 223 | "type": "array", 224 | "description": "Optional arguments passed to the runtime executable.", 225 | "items": { 226 | "type": "string" 227 | }, 228 | "default": [] 229 | }, 230 | "env": { 231 | "type": "object", 232 | "additionalProperties": { 233 | "type": "string" 234 | }, 235 | "description": "Environment variables passed to the program.", 236 | "default": {} 237 | }, 238 | "envFile": { 239 | "type": "string", 240 | "description": "Absolute path to a file containing environment variable definitions." 241 | }, 242 | "hostname": { 243 | "type": "string", 244 | "description": "Address to bind to when listening for Xdebug or Unix domain socket (start with unix://)", 245 | "default": "::" 246 | }, 247 | "port": { 248 | "type": "number", 249 | "description": "Port on which to listen for Xdebug", 250 | "default": 9003 251 | }, 252 | "serverSourceRoot": { 253 | "type": "string", 254 | "description": "Deprecated: The source root when debugging a remote host", 255 | "deprecationMessage": "Property serverSourceRoot is deprecated, please use pathMappings to define a server root." 256 | }, 257 | "localSourceRoot": { 258 | "type": "string", 259 | "description": "Deprecated: The source root on this machine that is the equivalent to the serverSourceRoot on the server.", 260 | "deprecationMessage": "Property localSourceRoot is deprecated, please use pathMappings to define a local root." 261 | }, 262 | "pathMappings": { 263 | "type": "object", 264 | "default": {}, 265 | "description": "A mapping of server paths to local paths." 266 | }, 267 | "ignore": { 268 | "type": "array", 269 | "items": "string", 270 | "description": "Array of glob patterns that errors should be ignored from", 271 | "default": [ 272 | "**/vendor/**/*.php" 273 | ] 274 | }, 275 | "skipFiles": { 276 | "type": "array", 277 | "items": "string", 278 | "description": "An array of glob patterns, to skip when debugging. Star patterns and negations are allowed, for example, `[\"**/vendor/**\", \"!**/vendor/my-module/**\"]`", 279 | "default": [ 280 | "**/vendor/**" 281 | ] 282 | }, 283 | "ignoreExceptions": { 284 | "type": "array", 285 | "items": "string", 286 | "description": "An array of exception class names that should be ignored." 287 | }, 288 | "skipEntryPaths": { 289 | "type": "array", 290 | "items": "string", 291 | "description": "An array of glob pattern to skip if the initial entry file is matched." 292 | }, 293 | "log": { 294 | "type": "boolean", 295 | "description": "If true, will log all communication between VS Code and the adapter" 296 | }, 297 | "proxy": { 298 | "type": "object", 299 | "properties": { 300 | "allowMultipleSessions": { 301 | "type": "boolean", 302 | "description": "If the proxy should expect multiple sessions/connections or not.", 303 | "default": true 304 | }, 305 | "enable": { 306 | "type": "boolean", 307 | "description": "Whether to enable usage of a proxy", 308 | "default": false 309 | }, 310 | "host": { 311 | "type": "string", 312 | "description": "Selects the host where the debug client is running, you can either use a host name, IP address, or 'unix:///path/to/sock' for a Unix domain socket. This setting is ignored if xdebug.remote_connect_back is enabled.", 313 | "default": "127.0.0.1" 314 | }, 315 | "key": { 316 | "type": "string", 317 | "description": "A unique key that allows the proxy to match requests to your editor", 318 | "default": "${config:php.debug.ideKey}" 319 | }, 320 | "port": { 321 | "type": "number", 322 | "description": "The port where the adapter will register with the the proxy.", 323 | "default": 9001 324 | }, 325 | "timeout": { 326 | "type": "number", 327 | "description": "The port where the adapter will register with the the proxy.", 328 | "default": 3000 329 | } 330 | } 331 | }, 332 | "xdebugSettings": { 333 | "type": "object", 334 | "properties": { 335 | "max_children": { 336 | "type": "integer", 337 | "description": "max number of array or object children to initially retrieve" 338 | }, 339 | "max_data": { 340 | "type": "integer", 341 | "description": "max amount of variable data to initially retrieve" 342 | }, 343 | "max_depth": { 344 | "type": "integer", 345 | "description": "maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE" 346 | }, 347 | "show_hidden": { 348 | "enum": [ 349 | 0, 350 | 1 351 | ], 352 | "description": "This feature can get set by the IDE if it wants to have more detailed internal information on properties (eg. private members of classes, etc.) Zero means that hidden members are not shown to the IDE" 353 | }, 354 | "breakpoint_include_return_value": { 355 | "enum": [ 356 | 0, 357 | 1 358 | ], 359 | "description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns." 360 | } 361 | }, 362 | "description": "Overrides for Xdebug remote debugging settings. See https://xdebug.org/docs-dbgp.php#feature-names", 363 | "default": {} 364 | }, 365 | "maxConnections": { 366 | "type": "number", 367 | "description": "The maximum allowed parallel debugging sessions", 368 | "default": 0 369 | }, 370 | "xdebugCloudToken": { 371 | "type": "string", 372 | "description": "Xdebug Could token" 373 | }, 374 | "stream": { 375 | "type": "object", 376 | "description": "Xdebug stream settings", 377 | "properties": { 378 | "stdout": { 379 | "type": "number", 380 | "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)", 381 | "default": 0 382 | } 383 | } 384 | } 385 | } 386 | } 387 | }, 388 | "initialConfigurations": [ 389 | { 390 | "name": "Listen for Xdebug", 391 | "type": "php", 392 | "request": "launch", 393 | "port": 9003 394 | }, 395 | { 396 | "name": "Launch currently open script", 397 | "type": "php", 398 | "request": "launch", 399 | "program": "${file}", 400 | "cwd": "${fileDirname}", 401 | "port": 0, 402 | "runtimeArgs": [ 403 | "-dxdebug.start_with_request=yes" 404 | ], 405 | "env": { 406 | "XDEBUG_MODE": "debug,develop", 407 | "XDEBUG_CONFIG": "client_port=${port}" 408 | } 409 | }, 410 | { 411 | "name": "Launch Built-in web server", 412 | "type": "php", 413 | "request": "launch", 414 | "runtimeArgs": [ 415 | "-dxdebug.mode=debug", 416 | "-dxdebug.start_with_request=yes", 417 | "-S", 418 | "localhost:0" 419 | ], 420 | "program": "", 421 | "cwd": "${workspaceRoot}", 422 | "port": 9003, 423 | "serverReadyAction": { 424 | "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", 425 | "uriFormat": "http://localhost:%s", 426 | "action": "openExternally" 427 | } 428 | } 429 | ], 430 | "configurationSnippets": [ 431 | { 432 | "label": "PHP: Listen for Xdebug", 433 | "description": "Listen for incoming XDebug connections", 434 | "body": { 435 | "name": "Listen for Xdebug", 436 | "type": "php", 437 | "request": "launch", 438 | "port": 9003 439 | } 440 | }, 441 | { 442 | "label": "PHP: Listen for Xdebug 2 (Legacy)", 443 | "description": "Listen for incoming XDebug connections on legacy port", 444 | "body": { 445 | "name": "Listen for Xdebug 2 (Legacy)", 446 | "type": "php", 447 | "request": "launch", 448 | "port": 9000 449 | } 450 | }, 451 | { 452 | "label": "PHP: Launch currently open script", 453 | "description": "Debug the currently open PHP script in CLI mode", 454 | "body": { 455 | "name": "Launch currently open script", 456 | "type": "php", 457 | "request": "launch", 458 | "program": "^\"${1:\\${file\\}}\"", 459 | "cwd": "^\"${2:\\${fileDirname\\}}\"", 460 | "port": 0, 461 | "runtimeArgs": [ 462 | "-dxdebug.start_with_request=yes" 463 | ], 464 | "env": { 465 | "XDEBUG_MODE": "debug,develop", 466 | "XDEBUG_CONFIG": "^\"client_port=\\${port\\}\"" 467 | } 468 | } 469 | }, 470 | { 471 | "label": "PHP: Launch currently open script with Xdebug 2 (Legacy)", 472 | "description": "Debug the currently open PHP script in CLI mode", 473 | "body": { 474 | "name": "Launch currently open script with Xdebug 2 (Legacy)", 475 | "type": "php", 476 | "request": "launch", 477 | "program": "^\"${1:\\${file\\}}\"", 478 | "cwd": "^\"${2:\\${fileDirname\\}}\"", 479 | "port": 0, 480 | "runtimeArgs": [ 481 | "-dxdebug.remote_enable=yes", 482 | "-dxdebug.remote_autostart=yes" 483 | ], 484 | "env": { 485 | "XDEBUG_CONFIG": "^\"remote_port=\\${port\\}\"" 486 | } 487 | } 488 | }, 489 | { 490 | "label": "PHP: Launch Built-in web server", 491 | "description": "Start built-in PHP web server and open browser on debug start", 492 | "body": { 493 | "name": "Launch Built-in web server", 494 | "type": "php", 495 | "request": "launch", 496 | "runtimeArgs": [ 497 | "-dxdebug.mode=debug", 498 | "-dxdebug.start_with_request=yes", 499 | "-S", 500 | "localhost:${1:0}" 501 | ], 502 | "program": "", 503 | "cwd": "^\"${2:\\${workspaceRoot\\}}\"", 504 | "port": 9003, 505 | "serverReadyAction": { 506 | "pattern": "Development Server \\\\(http://localhost:([0-9]+)\\\\) started", 507 | "uriFormat": "http://localhost:%s", 508 | "action": "openExternally" 509 | } 510 | } 511 | }, 512 | { 513 | "label": "PHP: Xdebug Cloud", 514 | "description": "Register with Xdebug Cloud and wait for debug sessions", 515 | "body": { 516 | "name": "Xdebug Cloud", 517 | "type": "php", 518 | "request": "launch", 519 | "xdebugCloudToken": "${1}" 520 | } 521 | } 522 | ] 523 | } 524 | ], 525 | "configuration": { 526 | "title": "PHP Debug", 527 | "properties": { 528 | "php.debug.executablePath": { 529 | "type": [ 530 | "string", 531 | "null" 532 | ], 533 | "default": null, 534 | "description": "The path to a PHP executable.", 535 | "scope": "machine-overridable" 536 | }, 537 | "php.debug.ideKey": { 538 | "type": "string", 539 | "default": "vsc", 540 | "description": "A unique key that allows the proxy to match requests to your editor. Only used when proxy configuration includes replacement.", 541 | "scope": "machine-overridable" 542 | } 543 | } 544 | }, 545 | "menus": { 546 | "editor/title/run": [ 547 | { 548 | "command": "extension.php-debug.runEditorContents", 549 | "when": "resourceLangId == php && !inDiffEditor && resourceScheme == file", 550 | "group": "navigation@1" 551 | }, 552 | { 553 | "command": "extension.php-debug.debugEditorContents", 554 | "when": "resourceLangId == php && !inDiffEditor && resourceScheme == file", 555 | "group": "navigation@2" 556 | } 557 | ], 558 | "commandPalette": [ 559 | { 560 | "command": "extension.php-debug.debugEditorContents", 561 | "when": "resourceLangId == php && !inDiffEditor && resourceScheme == file" 562 | }, 563 | { 564 | "command": "extension.php-debug.runEditorContents", 565 | "when": "resourceLangId == php && !inDiffEditor && resourceScheme == file" 566 | } 567 | ] 568 | }, 569 | "commands": [ 570 | { 571 | "command": "extension.php-debug.startWithStopOnEntry", 572 | "title": "Start Debugging and Stop on Entry", 573 | "category": "Debug" 574 | }, 575 | { 576 | "command": "extension.php-debug.debugEditorContents", 577 | "title": "Debug PHP File", 578 | "category": "PHP Debug", 579 | "enablement": "!inDebugMode", 580 | "icon": "$(debug-alt)" 581 | }, 582 | { 583 | "command": "extension.php-debug.runEditorContents", 584 | "title": "Run PHP File", 585 | "category": "PHP Debug", 586 | "enablement": "!inDebugMode", 587 | "icon": "$(play)" 588 | } 589 | ], 590 | "keybindings": [ 591 | { 592 | "command": "extension.php-debug.startWithStopOnEntry", 593 | "key": "F10", 594 | "when": "!inDebugMode && debugConfigurationType == 'php'" 595 | }, 596 | { 597 | "command": "extension.php-debug.startWithStopOnEntry", 598 | "key": "F11", 599 | "when": "!inDebugMode && activeViewlet == 'workbench.view.debug' && debugConfigurationType == 'php'" 600 | } 601 | ] 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:recommended"], 3 | "timezone": "Europe/Berlin", 4 | "rangeStrategy": "bump", 5 | "semanticCommits": "enabled", 6 | "dependencyDashboard": true, 7 | "prHourlyLimit": 0, 8 | "customManagers": [ 9 | { 10 | "customType": "regex", 11 | "fileMatch": ["^.github/workflows/.+\\.ya?ml$"], 12 | "matchStrings": ["xdebug: xdebug-(?\\S+).*# renovate:keep-up-to-date"], 13 | "depNameTemplate": "xdebug", 14 | "packageNameTemplate": "xdebug/xdebug", 15 | "datasourceTemplate": "github-tags", 16 | "versioningTemplate": "regex:^(?\\d+)\\.(?\\d+)\\.(?\\d+)(?\\w+)?$" 17 | }, 18 | { 19 | "customType": "regex", 20 | "fileMatch": ["^.github/workflows/.+\\.ya?ml$"], 21 | "matchStrings": ["php: '(?[^']+)'.*# renovate:keep-up-to-date"], 22 | "depNameTemplate": "php", 23 | "packageNameTemplate": "php/php-src", 24 | "datasourceTemplate": "github-tags", 25 | "versioningTemplate": "regex:^(?\\d+)\\.(?\\d+)\\.?(?\\d+)?(?\\w+)?$" 26 | }, 27 | { 28 | "customType": "regex", 29 | "fileMatch": ["^.github/workflows/.+\\.ya?ml$"], 30 | "matchStrings": ["node-version: '(?[^']+)'.*# renovate:keep-up-to-date"], 31 | "depNameTemplate": "node", 32 | "packageNameTemplate": "nodejs/node", 33 | "datasourceTemplate": "github-tags", 34 | "versioningTemplate": "node" 35 | } 36 | ], 37 | "packageRules": [ 38 | { 39 | "matchCategories": ["node"], 40 | "major": { 41 | "enabled": true 42 | } 43 | }, 44 | { 45 | "matchPackageNames": ["php"], 46 | "extractVersion": "^php-(?.*)$" 47 | }, 48 | { 49 | "matchPackageNames": ["node"], 50 | "extractVersion": "^v(?.*)$", 51 | "commitMessageTopic": "Node.js", 52 | "major": { 53 | "enabled": true 54 | } 55 | }, 56 | { 57 | "groupName": "vscode-debug", 58 | "matchPackageNames": ["/^vscode-debug/"] 59 | }, 60 | { 61 | "matchPackageNames": ["vscode"], 62 | "allowedVersions": "!/^1\\.999\\.0$/" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/TerminalHelper.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdebug/vscode-php-debug/8d2a50e0f1d43683b3ea306d5057562d030e02c1/src/TerminalHelper.scpt -------------------------------------------------------------------------------- /src/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { DebugProtocol as VSCodeDebugProtocol } from '@vscode/debugprotocol' 2 | import * as vscode from '@vscode/debugadapter' 3 | import { EventEmitter } from 'events' 4 | import * as xdebug from './xdebugConnection' 5 | import * as util from 'util' 6 | 7 | export declare interface BreakpointManager { 8 | on(event: 'add', listener: (breakpoints: Map) => void): this 9 | on(event: 'remove', listener: (breakpointIds: number[]) => void): this 10 | on(event: 'process', listener: () => void): this 11 | } 12 | 13 | /** 14 | * Keeps track of VS Code breakpoint IDs and maps them to Xdebug breakpoints. 15 | * Emits changes of breakpoints to BreakpointAdapter. 16 | */ 17 | export class BreakpointManager extends EventEmitter { 18 | private _lineBreakpoints = new Map>() 19 | private _exceptionBreakpoints = new Map() 20 | private _callBreakpoints = new Map() 21 | 22 | private _nextId = 1 23 | 24 | protected sourceKey(source: VSCodeDebugProtocol.Source): string { 25 | return source.path! 26 | } 27 | 28 | public setBreakPoints( 29 | source: VSCodeDebugProtocol.Source, 30 | fileUri: string, 31 | breakpoints: VSCodeDebugProtocol.SourceBreakpoint[] 32 | ): VSCodeDebugProtocol.Breakpoint[] { 33 | // let vscodeBreakpoints: VSCodeDebugProtocol.Breakpoint[] 34 | let toAdd = new Map() 35 | const toRemove: number[] = [] 36 | 37 | const sourceKey = this.sourceKey(source) 38 | 39 | // remove all existing breakpoints in the file 40 | if (this._lineBreakpoints.has(sourceKey)) { 41 | this._lineBreakpoints.get(sourceKey)?.forEach((_, key) => toRemove.push(key)) 42 | } 43 | 44 | // clear all breakpoints in this path 45 | const sourceBreakpoints = new Map() 46 | this._lineBreakpoints.set(sourceKey, sourceBreakpoints) 47 | 48 | const vscodeBreakpoints = breakpoints.map(sourceBreakpoint => { 49 | let xdebugBreakpoint: xdebug.Breakpoint 50 | let hitValue: number | undefined 51 | let hitCondition: xdebug.HitCondition | undefined 52 | if (sourceBreakpoint.hitCondition) { 53 | const match = sourceBreakpoint.hitCondition.match(/^\s*(>=|==|%)?\s*(\d+)\s*$/) 54 | if (match) { 55 | hitCondition = (match[1] as xdebug.HitCondition) || '==' 56 | hitValue = parseInt(match[2]) 57 | } else { 58 | const vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = { 59 | verified: false, 60 | line: sourceBreakpoint.line, 61 | source: source, 62 | // id: this._nextId++, 63 | message: 64 | 'Invalid hit condition. Specify a number, optionally prefixed with one of the operators >= (default), == or %', 65 | } 66 | return vscodeBreakpoint 67 | } 68 | } 69 | if (sourceBreakpoint.condition) { 70 | xdebugBreakpoint = new xdebug.ConditionalBreakpoint( 71 | sourceBreakpoint.condition, 72 | fileUri, 73 | sourceBreakpoint.line, 74 | hitCondition, 75 | hitValue 76 | ) 77 | } else { 78 | xdebugBreakpoint = new xdebug.LineBreakpoint(fileUri, sourceBreakpoint.line, hitCondition, hitValue) 79 | } 80 | 81 | const vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = { 82 | verified: this.listeners('add').length === 0, 83 | line: sourceBreakpoint.line, 84 | source: source, 85 | id: this._nextId++, 86 | } 87 | 88 | sourceBreakpoints.set(vscodeBreakpoint.id!, xdebugBreakpoint) 89 | 90 | return vscodeBreakpoint 91 | }) 92 | 93 | toAdd = sourceBreakpoints 94 | 95 | if (toRemove.length > 0) { 96 | this.emit('remove', toRemove) 97 | } 98 | if (toAdd.size > 0) { 99 | this.emit('add', toAdd) 100 | } 101 | 102 | return vscodeBreakpoints 103 | } 104 | 105 | public setExceptionBreakPoints(filters: string[]): VSCodeDebugProtocol.Breakpoint[] { 106 | const vscodeBreakpoints: VSCodeDebugProtocol.Breakpoint[] = [] 107 | let toAdd = new Map() 108 | const toRemove: number[] = [] 109 | 110 | // always remove all breakpoints 111 | this._exceptionBreakpoints.forEach((_, key) => toRemove.push(key)) 112 | this._exceptionBreakpoints.clear() 113 | 114 | filters.forEach(filter => { 115 | const xdebugBreakpoint: xdebug.Breakpoint = new xdebug.ExceptionBreakpoint(filter) 116 | const vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = { 117 | verified: this.listeners('add').length === 0, 118 | id: this._nextId++, 119 | } 120 | this._exceptionBreakpoints.set(vscodeBreakpoint.id!, xdebugBreakpoint) 121 | vscodeBreakpoints.push(vscodeBreakpoint) 122 | }) 123 | 124 | toAdd = this._exceptionBreakpoints 125 | 126 | if (toRemove.length > 0) { 127 | this.emit('remove', toRemove) 128 | } 129 | if (toAdd.size > 0) { 130 | this.emit('add', toAdd) 131 | } 132 | 133 | return vscodeBreakpoints 134 | } 135 | 136 | public setFunctionBreakPointsRequest( 137 | breakpoints: VSCodeDebugProtocol.FunctionBreakpoint[] 138 | ): VSCodeDebugProtocol.Breakpoint[] { 139 | let vscodeBreakpoints: VSCodeDebugProtocol.Breakpoint[] = [] 140 | let toAdd = new Map() 141 | const toRemove: number[] = [] 142 | 143 | // always remove all breakpoints 144 | this._callBreakpoints.forEach((_, key) => toRemove.push(key)) 145 | this._callBreakpoints.clear() 146 | 147 | vscodeBreakpoints = breakpoints.map(functionBreakpoint => { 148 | let hitValue: number | undefined 149 | let hitCondition: xdebug.HitCondition | undefined 150 | if (functionBreakpoint.hitCondition) { 151 | const match = functionBreakpoint.hitCondition.match(/^\s*(>=|==|%)?\s*(\d+)\s*$/) 152 | if (match) { 153 | hitCondition = (match[1] as xdebug.HitCondition) || '==' 154 | hitValue = parseInt(match[2]) 155 | } else { 156 | const vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = { 157 | verified: false, 158 | // id: this._nextId++, 159 | message: 160 | 'Invalid hit condition. Specify a number, optionally prefixed with one of the operators >= (default), == or %', 161 | } 162 | return vscodeBreakpoint 163 | } 164 | } 165 | const xdebugBreakpoint: xdebug.Breakpoint = new xdebug.CallBreakpoint( 166 | functionBreakpoint.name, 167 | functionBreakpoint.condition, 168 | hitCondition, 169 | hitValue 170 | ) 171 | 172 | const vscodeBreakpoint: VSCodeDebugProtocol.Breakpoint = { 173 | verified: this.listeners('add').length === 0, 174 | id: this._nextId++, 175 | } 176 | this._callBreakpoints.set(vscodeBreakpoint.id!, xdebugBreakpoint) 177 | return vscodeBreakpoint 178 | }) 179 | 180 | toAdd = this._callBreakpoints 181 | 182 | if (toRemove.length > 0) { 183 | this.emit('remove', toRemove) 184 | } 185 | if (toAdd.size > 0) { 186 | this.emit('add', toAdd) 187 | } 188 | 189 | return vscodeBreakpoints 190 | } 191 | 192 | public process(): void { 193 | // this will trigger a process on all adapters 194 | this.emit('process') 195 | } 196 | 197 | public getAll(): Map { 198 | const toAdd = new Map() 199 | for (const [_, lbp] of this._lineBreakpoints) { 200 | for (const [id, bp] of lbp) { 201 | toAdd.set(id, bp) 202 | } 203 | } 204 | for (const [id, bp] of this._exceptionBreakpoints) { 205 | toAdd.set(id, bp) 206 | } 207 | for (const [id, bp] of this._callBreakpoints) { 208 | toAdd.set(id, bp) 209 | } 210 | return toAdd 211 | } 212 | } 213 | 214 | interface AdapterBreakpoint { 215 | xdebugBreakpoint?: xdebug.Breakpoint 216 | state: 'add' | 'remove' | '' 217 | xdebugId?: number 218 | } 219 | 220 | export declare interface BreakpointAdapter { 221 | on( 222 | event: 'dapEvent', 223 | listener: (event: VSCodeDebugProtocol.BreakpointEvent | VSCodeDebugProtocol.OutputEvent) => void 224 | ): this 225 | } 226 | 227 | /** 228 | * Listens to changes from BreakpointManager and delivers them their own Xdebug Connection. 229 | * If DBGp connection is busy, track changes locally. 230 | */ 231 | export class BreakpointAdapter extends EventEmitter { 232 | private _connection: xdebug.Connection 233 | private _breakpointManager: BreakpointManager 234 | private _map = new Map() 235 | private _queue: (() => void)[] = [] 236 | private _executing = false 237 | 238 | constructor(connection: xdebug.Connection, breakpointManager: BreakpointManager) { 239 | super() 240 | this._connection = connection 241 | this._breakpointManager = breakpointManager 242 | this._add(breakpointManager.getAll()) 243 | // listeners 244 | this._breakpointManager.on('add', this._add) 245 | this._breakpointManager.on('remove', this._remove) 246 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 247 | this._breakpointManager.on('process', this.process) 248 | this._connection.on('close', (error?: Error) => { 249 | this._breakpointManager.off('add', this._add) 250 | this._breakpointManager.off('remove', this._remove) 251 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 252 | this._breakpointManager.off('process', this.process) 253 | }) 254 | this._connection.on('notify_breakpoint_resolved', this._notify) 255 | } 256 | 257 | protected _add = (breakpoints: Map): void => { 258 | breakpoints.forEach((xbp, id) => { 259 | this._queue.push(() => this._map.set(id, { xdebugBreakpoint: xbp, state: 'add' })) 260 | }) 261 | } 262 | 263 | protected _remove = (breakpointIds: number[]): void => { 264 | breakpointIds.forEach(id => { 265 | this._queue.push(() => { 266 | if (this._map.has(id)) { 267 | const bp = this._map.get(id)! 268 | if (!bp.xdebugId) { 269 | // has not been set 270 | this._map.delete(id) 271 | return 272 | } 273 | bp.state = 'remove' 274 | } 275 | }) 276 | }) 277 | } 278 | 279 | protected _notify = (notify: xdebug.BreakpointResolvedNotify): void => { 280 | if ( 281 | notify.breakpoint.resolved === 'resolved' && 282 | (notify.breakpoint instanceof xdebug.LineBreakpoint || 283 | notify.breakpoint instanceof xdebug.ConditionalBreakpoint) 284 | ) { 285 | Array.from(this._map.entries()) 286 | .filter(([id, abp]) => abp.xdebugId === notify.breakpoint.id) 287 | .map(([id, abp]) => { 288 | this.emit( 289 | 'dapEvent', 290 | new vscode.BreakpointEvent('changed', { 291 | id: id, 292 | verified: true, 293 | line: (notify.breakpoint).line, 294 | } as VSCodeDebugProtocol.Breakpoint) 295 | ) 296 | }) 297 | } 298 | } 299 | 300 | private _processPromise: Promise 301 | 302 | public process = (): Promise => { 303 | if (this._executing) { 304 | return this._processPromise 305 | } 306 | this._processPromise = this.__process() 307 | return this._processPromise 308 | } 309 | 310 | protected __process = async (): Promise => { 311 | if (this._executing) { 312 | // Protect from re-entry 313 | return 314 | } 315 | 316 | try { 317 | // Protect from re-entry 318 | this._executing = true 319 | 320 | // first execute all map modifying operations 321 | while (this._queue.length > 0) { 322 | const f = this._queue.shift()! 323 | f() 324 | } 325 | 326 | // do not execute network operations until network channel available 327 | if (this._connection.isPendingExecuteCommand) { 328 | return 329 | } 330 | 331 | for (const [id, abp] of this._map) { 332 | if (abp.state === 'remove') { 333 | try { 334 | await this._connection.sendBreakpointRemoveCommand(abp.xdebugId!) 335 | } catch (err) { 336 | this.emit('dapEvent', new vscode.OutputEvent(util.inspect(err) + '\n')) 337 | } 338 | this._map.delete(id) 339 | } 340 | } 341 | for (const [id, abp] of this._map) { 342 | if (abp.state === 'add') { 343 | try { 344 | const ret = await this._connection.sendBreakpointSetCommand(abp.xdebugBreakpoint!) 345 | this._map.set(id, { xdebugId: ret.breakpointId, state: '' }) 346 | const extra: { line?: number } = {} 347 | if ( 348 | ret.resolved === 'resolved' && 349 | (abp.xdebugBreakpoint!.type === 'line' || abp.xdebugBreakpoint!.type === 'conditional') 350 | ) { 351 | const bp = await this._connection.sendBreakpointGetCommand(ret.breakpointId) 352 | extra.line = (bp.breakpoint).line 353 | } 354 | // TODO copy original breakpoint object 355 | this.emit( 356 | 'dapEvent', 357 | new vscode.BreakpointEvent('changed', { 358 | id: id, 359 | verified: ret.resolved !== 'unresolved', 360 | ...extra, 361 | } as VSCodeDebugProtocol.Breakpoint) 362 | ) 363 | } catch (err) { 364 | this.emit('dapEvent', new vscode.OutputEvent(util.inspect(err) + '\n')) 365 | // TODO copy original breakpoint object 366 | this.emit( 367 | 'dapEvent', 368 | new vscode.BreakpointEvent('changed', { 369 | id: id, 370 | verified: false, 371 | message: (err).message, 372 | } as VSCodeDebugProtocol.Breakpoint) 373 | ) 374 | } 375 | } 376 | } 377 | } catch (error) { 378 | this.emit('dapEvent', new vscode.OutputEvent(util.inspect(error) + '\n')) 379 | } finally { 380 | this._executing = false 381 | } 382 | 383 | // If there were any concurrent changes to the op-queue, rerun processing right away 384 | if (this._queue.length > 0) { 385 | return await this.__process() 386 | } 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/cloud.ts: -------------------------------------------------------------------------------- 1 | import * as crc32 from 'buffer-crc32' 2 | import * as net from 'net' 3 | import { Transport, DbgpConnection, ENCODING } from './dbgp' 4 | import * as tls from 'tls' 5 | import * as iconv from 'iconv-lite' 6 | import * as xdebug from './xdebugConnection' 7 | import { EventEmitter } from 'stream' 8 | 9 | export declare interface XdebugCloudConnection { 10 | on(event: 'error', listener: (error: Error) => void): this 11 | on(event: 'close', listener: () => void): this 12 | on(event: 'log', listener: (text: string) => void): this 13 | on(event: 'connection', listener: (conn: xdebug.Connection) => void): this 14 | } 15 | 16 | export class XdebugCloudConnection extends EventEmitter { 17 | private _token: string 18 | 19 | private _netSocket: net.Socket 20 | private _tlsSocket: net.Socket 21 | 22 | private _resolveFn: (() => void) | null 23 | private _rejectFn: ((error?: Error) => void) | null 24 | 25 | private _dbgpConnection: DbgpConnection 26 | 27 | private _logging = true 28 | 29 | constructor(token: string, testSocket?: net.Socket) { 30 | super() 31 | if (testSocket != null) { 32 | this._netSocket = testSocket 33 | this._tlsSocket = testSocket 34 | } else { 35 | this._netSocket = new net.Socket() 36 | this._tlsSocket = new tls.TLSSocket(this._netSocket) 37 | } 38 | this._token = token 39 | this._resolveFn = null 40 | this._rejectFn = null 41 | this._dbgpConnection = new DbgpConnection(this._tlsSocket) 42 | 43 | this._dbgpConnection.on('log', (text: string) => { 44 | if (this._logging) { 45 | this.emit('log', text) 46 | } 47 | }) 48 | 49 | this._dbgpConnection.on('message', (response: XMLDocument) => { 50 | if (response.documentElement.nodeName === 'cloudinit') { 51 | if (response.documentElement.firstChild && response.documentElement.firstChild.nodeName === 'error') { 52 | this._rejectFn?.( 53 | new Error(`Error in CloudInit ${response.documentElement.firstChild.textContent ?? ''}`) 54 | ) 55 | } else { 56 | this._resolveFn?.() 57 | } 58 | } else if (response.documentElement.nodeName === 'cloudstop') { 59 | if (response.documentElement.firstChild && response.documentElement.firstChild.nodeName === 'error') { 60 | this._rejectFn?.( 61 | new Error(`Error in CloudStop ${response.documentElement.firstChild.textContent ?? ''}`) 62 | ) 63 | } else { 64 | this._resolveFn?.() 65 | } 66 | } else if (response.documentElement.nodeName === 'init') { 67 | this._logging = false 68 | // spawn a new xdebug.Connection 69 | const cx = new xdebug.Connection(new InnerCloudTransport(this._tlsSocket)) 70 | cx.once('close', () => (this._logging = true)) 71 | cx.emit('message', response) 72 | this.emit('connection', cx) 73 | } 74 | }) 75 | 76 | this._dbgpConnection.on('error', (err: Error) => { 77 | this.emit('log', `dbgp error: ${err.toString()}`) 78 | this._rejectFn?.(err instanceof Error ? err : new Error(err)) 79 | }) 80 | /* 81 | this._netSocket.on('error', (err: Error) => { 82 | this.emit('log', `netSocket error ${err.toString()}`) 83 | this._rejectFn?.(err instanceof Error ? err : new Error(err)) 84 | }) 85 | */ 86 | 87 | /* 88 | this._netSocket.on('connect', () => { 89 | this.emit('log', `netSocket connected`) 90 | // this._resolveFn?.() 91 | }) 92 | this._tlsSocket.on('secureConnect', () => { 93 | this.emit('log', `tlsSocket secureConnect`) 94 | //this._resolveFn?.() 95 | }) 96 | */ 97 | 98 | /* 99 | this._netSocket.on('close', had_error => { 100 | this.emit('log', 'netSocket close') 101 | this._rejectFn?.() // err instanceof Error ? err : new Error(err)) 102 | }) 103 | this._tlsSocket.on('close', had_error => { 104 | this.emit('log', 'tlsSocket close') 105 | this._rejectFn?.() 106 | }) 107 | */ 108 | this._dbgpConnection.on('close', () => { 109 | this.emit('log', `dbgp close`) 110 | this._rejectFn?.() // err instanceof Error ? err : new Error(err)) 111 | this.emit('close') 112 | }) 113 | } 114 | 115 | private computeCloudHost(token: string): string { 116 | const c = crc32.default(token) 117 | const last = c[3] & 0x0f 118 | const url = `${String.fromCharCode(97 + last)}.cloud.xdebug.com` 119 | 120 | return url 121 | } 122 | 123 | public async connect(): Promise { 124 | await new Promise((resolveFn, rejectFn) => { 125 | this._resolveFn = resolveFn 126 | this._rejectFn = rejectFn 127 | 128 | this._netSocket 129 | .connect( 130 | { 131 | host: this.computeCloudHost(this._token), 132 | servername: this.computeCloudHost(this._token), 133 | port: 9021, 134 | } as net.SocketConnectOpts, 135 | resolveFn 136 | ) 137 | .on('error', rejectFn) 138 | }) 139 | 140 | const commandString = `cloudinit -i 1 -u ${this._token}\0` 141 | const data = iconv.encode(commandString, ENCODING) 142 | 143 | const p2 = new Promise((resolveFn, rejectFn) => { 144 | this._resolveFn = resolveFn 145 | this._rejectFn = rejectFn 146 | }) 147 | 148 | await this._dbgpConnection.write(data) 149 | 150 | await p2 151 | } 152 | 153 | public async stop(): Promise { 154 | if (!this._tlsSocket.writable) { 155 | return Promise.resolve() 156 | } 157 | 158 | const commandString = `cloudstop -i 2 -u ${this._token}\0` 159 | const data = iconv.encode(commandString, ENCODING) 160 | 161 | const p2 = new Promise((resolveFn, rejectFn) => { 162 | this._resolveFn = resolveFn 163 | this._rejectFn = rejectFn 164 | }) 165 | 166 | await this._dbgpConnection.write(data) 167 | return p2 168 | } 169 | 170 | public async close(): Promise { 171 | return new Promise(resolve => { 172 | this._tlsSocket.end(resolve) 173 | }) 174 | } 175 | 176 | public async connectAndStop(): Promise { 177 | await new Promise((resolveFn, rejectFn) => { 178 | // this._resolveFn = resolveFn 179 | this._rejectFn = rejectFn 180 | this._netSocket 181 | .connect( 182 | { 183 | host: this.computeCloudHost(this._token), 184 | servername: this.computeCloudHost(this._token), 185 | port: 9021, 186 | } as net.SocketConnectOpts, 187 | resolveFn 188 | ) 189 | .on('error', rejectFn) 190 | }) 191 | await this.stop() 192 | await this.close() 193 | } 194 | } 195 | 196 | class InnerCloudTransport extends EventEmitter implements Transport { 197 | private _open = true 198 | 199 | constructor(private _socket: net.Socket) { 200 | super() 201 | 202 | this._socket.on('data', (data: Buffer) => { 203 | if (this._open) this.emit('data', data) 204 | }) 205 | this._socket.on('error', (error: Error) => { 206 | if (this._open) this.emit('error', error) 207 | }) 208 | this._socket.on('close', () => { 209 | if (this._open) this.emit('close') 210 | }) 211 | } 212 | 213 | public get writable(): boolean { 214 | return this._open && this._socket.writable 215 | } 216 | 217 | write(buffer: string | Uint8Array, cb?: ((err?: Error | undefined) => void) | undefined): boolean { 218 | return this._socket.write(buffer, cb) 219 | } 220 | 221 | end(callback?: (() => void) | undefined): this { 222 | if (this._open) { 223 | this._open = false 224 | this.emit('close') 225 | } 226 | return this 227 | } 228 | 229 | destroy(error?: Error): this { 230 | if (this._open) { 231 | this._open = false 232 | this.emit('close') 233 | } 234 | return this 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/dbgp.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import * as iconv from 'iconv-lite' 3 | import { DOMParser } from '@xmldom/xmldom' 4 | 5 | /** The encoding all Xdebug messages are encoded with */ 6 | export const ENCODING = 'utf-8' 7 | 8 | /** The two states the connection switches between */ 9 | enum ParsingState { 10 | DataLength, 11 | Response, 12 | } 13 | 14 | export interface Transport { 15 | readonly writable: boolean 16 | on(event: 'data', listener: (data: Buffer) => void): this 17 | on(event: 'error', listener: (error: Error) => void): this 18 | on(event: 'close', listener: () => void): this 19 | write(buffer: Uint8Array | string, cb?: (err?: Error) => void): boolean 20 | end(callback?: () => void): this 21 | destroy(error?: Error): this 22 | } 23 | 24 | export declare interface DbgpConnection { 25 | on(event: 'message', listener: (document: Document) => void): this 26 | on(event: 'error', listener: (error: Error) => void): this 27 | on(event: 'close', listener: () => void): this 28 | on(event: 'warning', listener: (warning: string) => void): this 29 | on(event: 'log', listener: (text: string) => void): this 30 | } 31 | 32 | /** Wraps the NodeJS Socket and calls handleResponse() whenever a full response arrives */ 33 | export class DbgpConnection extends EventEmitter { 34 | private _socket: Transport 35 | private _parsingState: ParsingState 36 | private _chunksDataLength: number 37 | private _chunks: Buffer[] 38 | private _dataLength: number 39 | private _closePromise: Promise 40 | private _closePromiseResolveFn: () => void 41 | 42 | constructor(socket: Transport) { 43 | super() 44 | this._socket = socket 45 | this._parsingState = ParsingState.DataLength 46 | this._chunksDataLength = 0 47 | this._chunks = [] 48 | this._closePromise = new Promise(resolve => (this._closePromiseResolveFn = resolve)) 49 | socket.on('data', (data: Buffer) => this._handleDataChunk(data)) 50 | socket.on('error', (error: Error) => this.emit('error', error)) 51 | socket.on('close', () => { 52 | this._closePromiseResolveFn?.() 53 | this.emit('close') 54 | }) 55 | } 56 | 57 | private _handleDataChunk(data: Buffer): void { 58 | // Anatomy of packets: [data length] [NULL] [xml] [NULL] 59 | // are we waiting for the data length or for the response? 60 | if (this._parsingState === ParsingState.DataLength) { 61 | // does data contain a NULL byte? 62 | const nullByteIndex = data.indexOf(0) 63 | if (nullByteIndex !== -1) { 64 | // YES -> we received the data length and are ready to receive the response 65 | const lastPiece = data.slice(0, nullByteIndex) 66 | this._chunks.push(lastPiece) 67 | this._chunksDataLength += lastPiece.length 68 | this._dataLength = parseInt( 69 | iconv.decode(Buffer.concat(this._chunks, this._chunksDataLength), ENCODING), 70 | 10 71 | ) 72 | // reset buffered chunks 73 | this._chunks = [] 74 | this._chunksDataLength = 0 75 | // switch to response parsing state 76 | this._parsingState = ParsingState.Response 77 | // if data contains more info (except the NULL byte) 78 | if (data.length > nullByteIndex + 1) { 79 | // handle the rest of the packet as part of the response 80 | const rest = data.slice(nullByteIndex + 1) 81 | this._handleDataChunk(rest) 82 | } 83 | } else { 84 | // NO -> this is only part of the data length. We wait for the next data event 85 | this._chunks.push(data) 86 | this._chunksDataLength += data.length 87 | } 88 | } else if (this._parsingState === ParsingState.Response) { 89 | // does the new data together with the buffered data add up to the data length? 90 | if (this._chunksDataLength + data.length >= this._dataLength) { 91 | // YES -> we received the whole response 92 | // append the last piece of the response 93 | const lastResponsePiece = data.slice(0, this._dataLength - this._chunksDataLength) 94 | this._chunks.push(lastResponsePiece) 95 | this._chunksDataLength += data.length 96 | const response = Buffer.concat(this._chunks, this._chunksDataLength) 97 | // call response handler 98 | const xml = iconv.decode(response, ENCODING) 99 | const parser = new DOMParser({ 100 | errorHandler: { 101 | warning: warning => { 102 | this.emit('warning', warning) 103 | }, 104 | error: error => { 105 | this.emit('error', error instanceof Error ? error : new Error(error as string)) 106 | }, 107 | fatalError: error => { 108 | this.emit('error', error instanceof Error ? error : new Error(error as string)) 109 | }, 110 | }, 111 | }) 112 | this.emit('log', `-> ${xml.replace(/[\0\n]/g, '')}`) 113 | const document = parser.parseFromString(xml, 'application/xml') 114 | this.emit('message', document) 115 | // reset buffer 116 | this._chunks = [] 117 | this._chunksDataLength = 0 118 | // switch to data length parsing state 119 | this._parsingState = ParsingState.DataLength 120 | // if data contains more info (except the NULL byte) 121 | if (data.length > lastResponsePiece.length + 1) { 122 | // handle the rest of the packet (after the NULL byte) as data length 123 | const rest = data.slice(lastResponsePiece.length + 1) 124 | this._handleDataChunk(rest) 125 | } 126 | } else { 127 | // NO -> this is not the whole response yet. We buffer it and wait for the next data event. 128 | this._chunks.push(data) 129 | this._chunksDataLength += data.length 130 | } 131 | } 132 | } 133 | 134 | public write(command: Buffer): Promise { 135 | return new Promise((resolve, reject) => { 136 | if (this._socket.writable) { 137 | this.emit('log', `<- ${command.toString().replace(/[\0\n]/g, '')}`) 138 | this._socket.write(command, () => { 139 | resolve() 140 | }) 141 | } else { 142 | reject(new Error('socket not writable')) 143 | } 144 | }) 145 | } 146 | 147 | /** closes the underlying socket */ 148 | public close(): Promise { 149 | this._socket.destroy() 150 | return this._closePromise 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/envfile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import { LaunchRequestArguments } from './phpDebug' 3 | import * as dotenv from 'dotenv' 4 | 5 | /** 6 | * Returns the user-configured portion of the environment variables. 7 | */ 8 | export function getConfiguredEnvironment(args: LaunchRequestArguments): { [key: string]: string } { 9 | if (args.envFile) { 10 | try { 11 | return merge(readEnvFile(args.envFile), args.env || {}) 12 | } catch (e) { 13 | throw new Error('Failed reading envFile') 14 | } 15 | } 16 | return args.env || {} 17 | } 18 | 19 | function readEnvFile(file: string): { [key: string]: string } { 20 | if (!fs.existsSync(file)) { 21 | return {} 22 | } 23 | const buffer = stripBOM(fs.readFileSync(file, 'utf8')) 24 | const env = dotenv.parse(Buffer.from(buffer)) 25 | return env 26 | } 27 | 28 | function stripBOM(s: string): string { 29 | if (s && s[0] === '\uFEFF') { 30 | s = s.substring(1) 31 | } 32 | return s 33 | } 34 | 35 | function merge(...vars: { [key: string]: string }[]): { [key: string]: string } { 36 | if (process.platform === 'win32') { 37 | return caseInsensitiveMerge(...vars) 38 | } 39 | return Object.assign({}, ...vars) as { [key: string]: string } 40 | } 41 | 42 | /** 43 | * Performs a case-insenstive merge of the list of objects. 44 | */ 45 | function caseInsensitiveMerge(...objs: ReadonlyArray | undefined | null>) { 46 | if (objs.length === 0) { 47 | return {} 48 | } 49 | const out: { [key: string]: V } = {} 50 | const caseMapping: { [key: string]: string } = Object.create(null) // prototype-free object 51 | for (const obj of objs) { 52 | if (!obj) { 53 | continue 54 | } 55 | for (const key of Object.keys(obj)) { 56 | const normalized = key.toLowerCase() 57 | if (caseMapping[normalized]) { 58 | out[caseMapping[normalized]] = obj[key] 59 | } else { 60 | caseMapping[normalized] = key 61 | out[key] = obj[key] 62 | } 63 | } 64 | } 65 | return out 66 | } 67 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import { WorkspaceFolder, DebugConfiguration, CancellationToken } from 'vscode' 3 | import { LaunchRequestArguments } from './phpDebug' 4 | import * as which from 'which' 5 | import * as path from 'path' 6 | 7 | export function activate(context: vscode.ExtensionContext) { 8 | context.subscriptions.push( 9 | vscode.debug.registerDebugConfigurationProvider('php', { 10 | async resolveDebugConfiguration( 11 | folder: WorkspaceFolder | undefined, 12 | debugConfiguration: DebugConfiguration & LaunchRequestArguments, 13 | token?: CancellationToken 14 | ): Promise { 15 | const isDynamic = 16 | (!debugConfiguration.type || debugConfiguration.type === 'php') && 17 | !debugConfiguration.request && 18 | !debugConfiguration.name 19 | if (isDynamic) { 20 | const editor = vscode.window.activeTextEditor 21 | if (editor && editor.document.languageId === 'php') { 22 | debugConfiguration.type = 'php' 23 | debugConfiguration.name = 'Launch (dynamic)' 24 | debugConfiguration.request = 'launch' 25 | debugConfiguration.program = debugConfiguration.program || '${file}' 26 | debugConfiguration.cwd = debugConfiguration.cwd || '${fileDirname}' 27 | debugConfiguration.port = 0 28 | debugConfiguration.runtimeArgs = ['-dxdebug.start_with_request=yes'] 29 | debugConfiguration.env = { 30 | XDEBUG_MODE: 'debug,develop', 31 | XDEBUG_CONFIG: 'client_port=${port}', 32 | } 33 | // debugConfiguration.stopOnEntry = true 34 | } 35 | } 36 | if ( 37 | (debugConfiguration.program || debugConfiguration.runtimeArgs) && 38 | !debugConfiguration.runtimeExecutable 39 | ) { 40 | // See if we have runtimeExecutable configured 41 | const conf = vscode.workspace.getConfiguration('php.debug') 42 | const executablePath = conf.get('executablePath') 43 | if (executablePath) { 44 | debugConfiguration.runtimeExecutable = executablePath 45 | } 46 | // See if it's in path 47 | if (!debugConfiguration.runtimeExecutable) { 48 | try { 49 | await which.default('php') 50 | } catch (e) { 51 | const selected = await vscode.window.showErrorMessage( 52 | 'PHP executable not found. Install PHP and add it to your PATH or set the php.debug.executablePath setting', 53 | 'Open settings' 54 | ) 55 | if (selected === 'Open settings') { 56 | await vscode.commands.executeCommand('workbench.action.openGlobalSettings', { 57 | query: 'php.debug.executablePath', 58 | }) 59 | return undefined 60 | } 61 | } 62 | } 63 | } 64 | if (debugConfiguration.proxy?.enable === true) { 65 | // Proxy configuration 66 | if (!debugConfiguration.proxy.key) { 67 | const conf = vscode.workspace.getConfiguration('php.debug') 68 | const ideKey = conf.get('ideKey') 69 | if (ideKey) { 70 | debugConfiguration.proxy.key = ideKey 71 | } 72 | } 73 | } 74 | if (folder && folder.uri.scheme !== 'file') { 75 | // replace 76 | if (debugConfiguration.pathMappings) { 77 | for (const key in debugConfiguration.pathMappings) { 78 | debugConfiguration.pathMappings[key] = debugConfiguration.pathMappings[key].replace( 79 | '${workspaceFolder}', 80 | folder.uri.toString() 81 | ) 82 | } 83 | } 84 | // The following path are currently NOT mapped 85 | /* 86 | debugConfiguration.skipEntryPaths = debugConfiguration.skipEntryPaths?.map(v => 87 | v.replace('${workspaceFolder}', folder.uri.toString()) 88 | ) 89 | debugConfiguration.skipFiles = debugConfiguration.skipFiles?.map(v => 90 | v.replace('${workspaceFolder}', folder.uri.toString()) 91 | ) 92 | debugConfiguration.ignore = debugConfiguration.ignore?.map(v => 93 | v.replace('${workspaceFolder}', folder.uri.toString()) 94 | ) 95 | */ 96 | } 97 | return debugConfiguration 98 | }, 99 | }) 100 | ) 101 | 102 | context.subscriptions.push( 103 | vscode.commands.registerCommand('extension.php-debug.runEditorContents', (resource: vscode.Uri) => { 104 | let targetResource = resource 105 | if (!targetResource && vscode.window.activeTextEditor) { 106 | targetResource = vscode.window.activeTextEditor.document.uri 107 | } 108 | if (targetResource) { 109 | void vscode.debug.startDebugging(undefined, { 110 | type: 'php', 111 | name: '', 112 | request: '', 113 | noDebug: true, 114 | program: targetResource.fsPath, 115 | cwd: path.dirname(targetResource.fsPath), 116 | }) 117 | } 118 | }), 119 | vscode.commands.registerCommand('extension.php-debug.debugEditorContents', (resource: vscode.Uri) => { 120 | let targetResource = resource 121 | if (!targetResource && vscode.window.activeTextEditor) { 122 | targetResource = vscode.window.activeTextEditor.document.uri 123 | } 124 | if (targetResource) { 125 | void vscode.debug.startDebugging(undefined, { 126 | type: 'php', 127 | name: '', 128 | request: '', 129 | stopOnEntry: true, 130 | program: targetResource.fsPath, 131 | cwd: path.dirname(targetResource.fsPath), 132 | }) 133 | } 134 | }) 135 | ) 136 | 137 | context.subscriptions.push( 138 | vscode.commands.registerCommand('extension.php-debug.startWithStopOnEntry', async (uri: vscode.Uri) => { 139 | await vscode.commands.executeCommand('workbench.action.debug.start', { 140 | config: { 141 | stopOnEntry: true, 142 | }, 143 | }) 144 | }) 145 | ) 146 | } 147 | -------------------------------------------------------------------------------- /src/ignore.ts: -------------------------------------------------------------------------------- 1 | export function shouldIgnoreException(name: string, patterns: string[]): boolean { 2 | return patterns.some(pattern => name.match(convertPattern(pattern))) 3 | } 4 | 5 | function convertPattern(pattern: string): string { 6 | const esc = pattern.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') 7 | const proc = esc.replace(/\\\*\\\*/g, '.*').replace(/\\\*/g, '[^\\\\]*') 8 | return '^' + proc + '$' 9 | } 10 | -------------------------------------------------------------------------------- /src/logpoint.ts: -------------------------------------------------------------------------------- 1 | import stringReplaceAsync from 'string-replace-async' 2 | import { isWindowsUri } from './paths' 3 | 4 | export class LogPointManager { 5 | private _logpoints = new Map>() 6 | 7 | public addLogPoint(fileUri: string, lineNumber: number, logMessage: string) { 8 | if (isWindowsUri(fileUri)) { 9 | fileUri = fileUri.toLowerCase() 10 | } 11 | if (!this._logpoints.has(fileUri)) { 12 | this._logpoints.set(fileUri, new Map()) 13 | } 14 | this._logpoints.get(fileUri)!.set(lineNumber, logMessage) 15 | } 16 | 17 | public clearFromFile(fileUri: string) { 18 | if (isWindowsUri(fileUri)) { 19 | fileUri = fileUri.toLowerCase() 20 | } 21 | if (this._logpoints.has(fileUri)) { 22 | this._logpoints.get(fileUri)!.clear() 23 | } 24 | } 25 | 26 | public hasLogPoint(fileUri: string, lineNumber: number): boolean { 27 | if (isWindowsUri(fileUri)) { 28 | fileUri = fileUri.toLowerCase() 29 | } 30 | return this._logpoints.has(fileUri) && this._logpoints.get(fileUri)!.has(lineNumber) 31 | } 32 | 33 | public async resolveExpressions( 34 | fileUri: string, 35 | lineNumber: number, 36 | callback: (expr: string) => Promise 37 | ): Promise { 38 | if (isWindowsUri(fileUri)) { 39 | fileUri = fileUri.toLowerCase() 40 | } 41 | if (!this.hasLogPoint(fileUri, lineNumber)) { 42 | return Promise.reject('Logpoint not found') 43 | } 44 | const expressionRegex = /\{(.*?)\}/gm 45 | return await stringReplaceAsync( 46 | this._logpoints.get(fileUri)!.get(lineNumber)!, 47 | expressionRegex, 48 | function (_: string, group: string) { 49 | return group.length === 0 ? Promise.resolve('') : callback(group) 50 | } 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/paths.ts: -------------------------------------------------------------------------------- 1 | import fileUrl from 'file-url' 2 | import * as url from 'url' 3 | import * as Path from 'path' 4 | import minimatch from 'minimatch' 5 | 6 | /** converts a server-side Xdebug file URI to a local path for VS Code with respect to source root settings */ 7 | export function convertDebuggerPathToClient(fileUri: string, pathMapping?: { [index: string]: string }): string { 8 | let localSourceRootUrl: string | undefined 9 | let serverSourceRootUrl: string | undefined 10 | 11 | if (pathMapping) { 12 | for (const mappedServerPath of Object.keys(pathMapping)) { 13 | let mappedServerPathUrl = pathOrUrlToUrl(mappedServerPath) 14 | // try exact match 15 | if (fileUri.length === mappedServerPathUrl.length && isSameUri(fileUri, mappedServerPathUrl)) { 16 | // bail early 17 | serverSourceRootUrl = mappedServerPathUrl 18 | localSourceRootUrl = pathOrUrlToUrl(pathMapping[mappedServerPath]) 19 | break 20 | } 21 | // make sure it ends with a slash 22 | if (!mappedServerPathUrl.endsWith('/')) { 23 | mappedServerPathUrl += '/' 24 | } 25 | if (isSameUri(fileUri.substring(0, mappedServerPathUrl.length), mappedServerPathUrl)) { 26 | // If a matching mapping has previously been found, only update 27 | // it if the current server path is longer than the previous one 28 | // (longest prefix matching) 29 | if (!serverSourceRootUrl || mappedServerPathUrl.length > serverSourceRootUrl.length) { 30 | serverSourceRootUrl = mappedServerPathUrl 31 | localSourceRootUrl = pathOrUrlToUrl(pathMapping[mappedServerPath]) 32 | if (!localSourceRootUrl.endsWith('/')) { 33 | localSourceRootUrl += '/' 34 | } 35 | } 36 | } 37 | } 38 | } 39 | let localPath: string 40 | if (serverSourceRootUrl && localSourceRootUrl) { 41 | fileUri = localSourceRootUrl + fileUri.substring(serverSourceRootUrl.length) 42 | } 43 | if (fileUri.startsWith('file://')) { 44 | const u = new URL(fileUri) 45 | let pathname = u.pathname 46 | if (isWindowsUri(fileUri)) { 47 | // From Node.js lib/internal/url.js pathToFileURL 48 | pathname = pathname.replace(/\//g, Path.win32.sep) 49 | pathname = decodeURIComponent(pathname) 50 | if (u.hostname !== '') { 51 | localPath = `\\\\${url.domainToUnicode(u.hostname)}${pathname}` 52 | } else { 53 | localPath = pathname.slice(1) 54 | } 55 | } else { 56 | localPath = decodeURIComponent(pathname) 57 | } 58 | } else { 59 | // if it's not a file url it could be sshfs or something else 60 | localPath = fileUri 61 | } 62 | return localPath 63 | } 64 | 65 | /** converts a local path from VS Code to a server-side Xdebug file URI with respect to source root settings */ 66 | export function convertClientPathToDebugger(localPath: string, pathMapping?: { [index: string]: string }): string { 67 | let localSourceRootUrl: string | undefined 68 | let serverSourceRootUrl: string | undefined 69 | 70 | // Parse or convert local path to URL 71 | const localFileUri = pathOrUrlToUrl(localPath) 72 | 73 | let serverFileUri: string 74 | if (pathMapping) { 75 | for (const mappedServerPath of Object.keys(pathMapping)) { 76 | //let mappedLocalSource = pathMapping[mappedServerPath] 77 | let mappedLocalSourceUrl = pathOrUrlToUrl(pathMapping[mappedServerPath]) 78 | // try exact match 79 | if (localFileUri.length === mappedLocalSourceUrl.length && isSameUri(localFileUri, mappedLocalSourceUrl)) { 80 | // bail early 81 | localSourceRootUrl = mappedLocalSourceUrl 82 | serverSourceRootUrl = pathOrUrlToUrl(mappedServerPath) 83 | break 84 | } 85 | // make sure it ends with a slash 86 | if (!mappedLocalSourceUrl.endsWith('/')) { 87 | mappedLocalSourceUrl += '/' 88 | } 89 | 90 | if (isSameUri(localFileUri.substring(0, mappedLocalSourceUrl.length), mappedLocalSourceUrl)) { 91 | // If a matching mapping has previously been found, only update 92 | // it if the current local path is longer than the previous one 93 | // (longest prefix matching) 94 | if (!localSourceRootUrl || mappedLocalSourceUrl.length > localSourceRootUrl.length) { 95 | localSourceRootUrl = mappedLocalSourceUrl 96 | serverSourceRootUrl = pathOrUrlToUrl(mappedServerPath) 97 | if (!serverSourceRootUrl.endsWith('/')) { 98 | serverSourceRootUrl += '/' 99 | } 100 | } 101 | } 102 | } 103 | } 104 | if (serverSourceRootUrl && localSourceRootUrl) { 105 | serverFileUri = serverSourceRootUrl + localFileUri.substring(localSourceRootUrl.length) 106 | } else { 107 | serverFileUri = localFileUri 108 | } 109 | return serverFileUri 110 | } 111 | 112 | export function isWindowsUri(path: string): boolean { 113 | return /^file:\/\/\/[a-zA-Z]:\//.test(path) || /^file:\/\/[^/]/.test(path) 114 | } 115 | 116 | function isWindowsPath(path: string): boolean { 117 | return /^[a-zA-Z]:\\/.test(path) || /^\\\\/.test(path) || /^[a-zA-Z]:$/.test(path) || /^[a-zA-Z]:\//.test(path) 118 | } 119 | 120 | function pathOrUrlToUrl(path: string): string { 121 | // Do not try to parse windows drive letter paths 122 | if (!isWindowsPath(path)) { 123 | try { 124 | // try to parse, but do not modify 125 | new URL(path).toString() 126 | // super simple relative path resolver 127 | return simpleResolveUrl(path) 128 | } catch (ex) { 129 | // should be a path 130 | } 131 | } 132 | // Not a URL, do some windows path mangling before it is converted to URL 133 | if (path.startsWith('\\\\')) { 134 | // UNC 135 | path = Path.win32.resolve(path) 136 | const hostEndIndex = path.indexOf('\\', 2) 137 | const host = path.substring(2, hostEndIndex) 138 | const outURL = new URL('file://') 139 | outURL.hostname = url.domainToASCII(host) 140 | outURL.pathname = path.substring(hostEndIndex).replace(/\\/g, '/') 141 | return outURL.toString() 142 | } 143 | if (/^[a-zA-Z]:$/.test(path)) { 144 | // if local source root mapping is only drive letter, add backslash 145 | path += '\\' 146 | } 147 | // Do not change drive later to lower case anymore 148 | // if (/^[a-zA-Z]:/.test(path)) { 149 | // // Xdebug always lowercases Windows drive letters in file URIs 150 | // //path = path.replace(/^[A-Z]:/, match => match.toLowerCase()) 151 | // } 152 | path = isWindowsPath(path) ? Path.win32.resolve(path) : Path.posix.resolve(path) 153 | return fileUrl(path, { resolve: false }) 154 | } 155 | 156 | export function isSameUri(clientUri: string, debuggerUri: string): boolean { 157 | if (isWindowsUri(clientUri) || isWindowsUri(debuggerUri)) { 158 | // compare case-insensitive on Windows 159 | return debuggerUri.toLowerCase() === clientUri.toLowerCase() 160 | } else { 161 | return debuggerUri === clientUri 162 | } 163 | } 164 | 165 | export function isPositiveMatchInGlobs(path: string, globs: string[]): boolean { 166 | const f = globs.find(glob => minimatch(path, glob.charAt(0) == '!' ? glob.substring(1) : glob)) 167 | return f !== undefined && f.charAt(0) !== '!' 168 | } 169 | 170 | function simpleResolveUrl(path: string): string { 171 | if (path.indexOf('/../') != -1) { 172 | const pp = path.split('/') 173 | let i 174 | while ((i = pp.findIndex(v => v == '..')) > 0) { 175 | pp.splice(i - 1, 2) 176 | } 177 | path = pp.join('/') 178 | } 179 | return path 180 | } 181 | -------------------------------------------------------------------------------- /src/proxyConnect.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net' 2 | import { DOMParser } from '@xmldom/xmldom' 3 | import { EventEmitter } from 'events' 4 | import { decode } from 'iconv-lite' 5 | import { ENCODING } from './dbgp' 6 | 7 | export const DEFAULTIDEKEY = 'vsc' 8 | 9 | export interface ProxyMessages { 10 | defaultError: string 11 | deregisterInfo: string 12 | deregisterSuccess: string 13 | duplicateKey: string 14 | nonexistentKey: string 15 | registerInfo: string 16 | registerSuccess: string 17 | resolve: string 18 | timeout: string 19 | raceCall: string 20 | } 21 | 22 | /** Informs proxy of incoming connection and who to pass data back to. */ 23 | export class ProxyConnect extends EventEmitter { 24 | /** Port editor is listening on (default: 9001 */ 25 | private _port: number 26 | /** a CLI binary boolean option (default: 1) */ 27 | private _allowMultipleSessions: number 28 | /** host domain or ip (default: 127.0.0.1) */ 29 | private _host: string 30 | /** ide port proxy will connect back */ 31 | private _idePort: number 32 | /** unique key that allows the proxy to match requests to your editor. (default: DEFAULTIDEKEY) */ 33 | private _key: string 34 | /** proxy response data parser */ 35 | private _parser = new DOMParser() 36 | /** tcp connection to communicate with proxy server */ 37 | private _socket: Socket 38 | /** milliseconds to wait before giving up */ 39 | private _timeout: number 40 | public msgs: ProxyMessages 41 | private _isRegistered = false 42 | private _resolveFn: (() => void) | null 43 | private _rejectFn: ((error?: Error) => void) | null 44 | private _chunksDataLength: number 45 | private _chunks: Buffer[] 46 | 47 | constructor( 48 | host = '127.0.0.1', 49 | port = 9001, 50 | idePort = 9003, 51 | allowMultipleSessions = true, 52 | key = DEFAULTIDEKEY, 53 | timeout = 3000, 54 | socket?: Socket 55 | ) { 56 | super() 57 | this._allowMultipleSessions = allowMultipleSessions ? 1 : 0 58 | this._host = host 59 | this._key = key 60 | this._port = port 61 | this._idePort = idePort 62 | this._timeout = timeout 63 | this._socket = socket ? socket : new Socket() 64 | this._chunksDataLength = 0 65 | this._chunks = [] 66 | this._resolveFn = null 67 | this._rejectFn = null 68 | this.msgs = { 69 | defaultError: 'Unknown proxy Error', 70 | deregisterInfo: `De-registering ${this._key} with proxy @ ${this._host}:${this._port}`, 71 | deregisterSuccess: 'De-registration successful', 72 | duplicateKey: 'IDE Key already exists', 73 | nonexistentKey: 'No IDE key', 74 | registerInfo: `Registering ${this._key} on port ${this._idePort} with proxy @ ${this._host}:${this._port}`, 75 | registerSuccess: 'Registration successful', 76 | resolve: `Failure to resolve ${this._host}`, 77 | timeout: `Timeout connecting to ${this._host}:${this._port}`, 78 | raceCall: 'New command before old finished', 79 | } 80 | this._socket.on('error', (err: Error) => { 81 | // Propagate error up 82 | this._socket.end() 83 | this.emit('log_error', err instanceof Error ? err : new Error(err)) 84 | this._rejectFn?.(err instanceof Error ? err : new Error(err)) 85 | }) 86 | this._socket.on('lookup', (err: Error | null, address: string, family: string | null, host: string) => { 87 | if (err instanceof Error) { 88 | this._socket.emit('error', this.msgs.resolve) 89 | } 90 | }) 91 | this._socket.on('data', data => { 92 | this._chunks.push(data) 93 | this._chunksDataLength += data.length 94 | }) 95 | this._socket.on('close', had_error => { 96 | if (!had_error) { 97 | this._responseStrategy(Buffer.concat(this._chunks, this._chunksDataLength)) 98 | } 99 | this._chunksDataLength = 0 100 | this._chunks = [] 101 | }) 102 | this._socket.setTimeout(this._timeout) 103 | this._socket.on('timeout', () => { 104 | this._socket.emit('error', this.msgs.timeout) 105 | }) 106 | } 107 | 108 | private _command(cmd: string, msg?: string) { 109 | this.emit('log_request', msg) 110 | this._socket.connect(this._port, this._host, () => { 111 | this._socket.write(cmd) 112 | new Promise(resolve => setTimeout(resolve, 500)) 113 | .then(() => { 114 | if (!this._socket.destroyed) { 115 | this._socket.write('\0') 116 | } 117 | }) 118 | .catch(err => { 119 | this._rejectFn?.(new Error(err as string)) 120 | }) 121 | }) 122 | } 123 | 124 | /** Register/Couples ideKey to IP so the proxy knows who to send what */ 125 | public sendProxyInitCommand(): Promise { 126 | this._rejectFn?.(new Error(this.msgs.raceCall)) 127 | return new Promise((resolveFn, rejectFn) => { 128 | if (!this._isRegistered) { 129 | this._resolveFn = resolveFn 130 | this._rejectFn = rejectFn 131 | this._command( 132 | `proxyinit -k ${this._key} -p ${this._idePort} -m ${this._allowMultipleSessions}`, 133 | this.msgs.registerInfo 134 | ) 135 | } else { 136 | resolveFn() 137 | } 138 | }) 139 | } 140 | 141 | /** De-registers/Decouples ideKey from IP, allowing others to use the ideKey */ 142 | public sendProxyStopCommand(): Promise { 143 | this._rejectFn?.(new Error(this.msgs.raceCall)) 144 | return new Promise((resolveFn, rejectFn) => { 145 | if (this._isRegistered) { 146 | this._resolveFn = resolveFn 147 | this._rejectFn = rejectFn 148 | this._command(`proxystop -k ${this._key}`, this.msgs.deregisterInfo) 149 | } else { 150 | resolveFn() 151 | } 152 | }) 153 | } 154 | 155 | /** Parse data from response server and emit the relevant notification. */ 156 | private _responseStrategy(data: Buffer) { 157 | try { 158 | const documentElement = this._parser.parseFromString( 159 | decode(data, ENCODING), 160 | 'application/xml' 161 | ).documentElement 162 | const isSuccessful = documentElement.getAttribute('success') === '1' 163 | const error = documentElement.firstChild 164 | if (isSuccessful && documentElement.nodeName === 'proxyinit') { 165 | this._isRegistered = true 166 | this.emit('log_response', this.msgs.registerSuccess) 167 | this._resolveFn?.() 168 | } else if (isSuccessful && documentElement.nodeName === 'proxystop') { 169 | this._isRegistered = false 170 | this.emit('log_response', this.msgs.deregisterSuccess) 171 | this._resolveFn?.() 172 | } else if (error && error.nodeName === 'error' && error.firstChild && error.firstChild.textContent) { 173 | this._socket.emit('error', error.firstChild.textContent) 174 | this._rejectFn?.(new Error(error.firstChild.textContent)) 175 | } else { 176 | this._socket.emit('error', this.msgs.defaultError) 177 | this._rejectFn?.(new Error(this.msgs.defaultError)) 178 | } 179 | } catch (error) { 180 | this._rejectFn?.( 181 | new Error(`Proxy read error ${error instanceof Error ? error.message : (error as string)}`) 182 | ) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/terminal.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as Path from 'path' 7 | import * as FS from 'fs' 8 | import * as CP from 'child_process' 9 | 10 | export class Terminal { 11 | private static _terminalService: ITerminalService 12 | 13 | public static launchInTerminal( 14 | dir: string, 15 | args: string[], 16 | envVars: { [key: string]: string | undefined } 17 | ): Promise { 18 | return this.terminalService().launchInTerminal(dir, args, envVars) 19 | } 20 | 21 | public static killTree(processId: number): Promise { 22 | return this.terminalService().killTree(processId) 23 | } 24 | 25 | /* 26 | * Is the given runtime executable on the PATH. 27 | */ 28 | public static isOnPath(program: string): boolean { 29 | return this.terminalService().isOnPath(program) 30 | } 31 | 32 | private static terminalService(): ITerminalService { 33 | if (!this._terminalService) { 34 | if (process.platform === 'win32') { 35 | this._terminalService = new WindowsTerminalService() 36 | } else if (process.platform === 'darwin') { 37 | this._terminalService = new MacTerminalService() 38 | } else if (process.platform === 'linux') { 39 | this._terminalService = new LinuxTerminalService() 40 | } else { 41 | this._terminalService = new DefaultTerminalService() 42 | } 43 | } 44 | return this._terminalService 45 | } 46 | } 47 | 48 | interface ITerminalService { 49 | launchInTerminal( 50 | dir: string, 51 | args: string[], 52 | envVars: { [key: string]: string | undefined } 53 | ): Promise 54 | killTree(pid: number): Promise 55 | isOnPath(program: string): boolean 56 | } 57 | 58 | class DefaultTerminalService implements ITerminalService { 59 | protected static TERMINAL_TITLE = 'VS Code Console' 60 | 61 | public launchInTerminal( 62 | dir: string, 63 | args: string[], 64 | envVars: { [key: string]: string } 65 | ): Promise { 66 | throw new Error('launchInTerminal not implemented') 67 | } 68 | 69 | public killTree(pid: number): Promise { 70 | // on linux and OS X we kill all direct and indirect child processes as well 71 | 72 | return new Promise((resolve, reject) => { 73 | try { 74 | const cmd = Path.join(__dirname, './terminateProcess.sh') 75 | const result = CP.spawnSync(cmd, [pid.toString()]) 76 | if (result.error) { 77 | reject(result.error) 78 | } else { 79 | resolve(undefined) 80 | } 81 | } catch (err) { 82 | reject(err) 83 | } 84 | }) 85 | } 86 | 87 | public isOnPath(program: string): boolean { 88 | /* 89 | var which = FS.existsSync(DefaultTerminalService.WHICH) ? DefaultTerminalService.WHICH : DefaultTerminalService.WHERE; 90 | var cmd = Utils.format('{0} \'{1}\'', which, program); 91 | 92 | try { 93 | CP.execSync(cmd); 94 | 95 | return process.ExitCode == 0; 96 | } 97 | catch (Exception) { 98 | // ignore 99 | } 100 | 101 | return false; 102 | */ 103 | 104 | return true 105 | } 106 | } 107 | 108 | class WindowsTerminalService extends DefaultTerminalService { 109 | private static CMD = 'cmd.exe' 110 | 111 | public launchInTerminal( 112 | dir: string, 113 | args: string[], 114 | envVars: { [key: string]: string } 115 | ): Promise { 116 | return new Promise((resolve, reject) => { 117 | const title = `"${dir} - ${WindowsTerminalService.TERMINAL_TITLE}"` 118 | const command = `""${args.join('" "')}" & pause"` // use '|' to only pause on non-zero exit code 119 | 120 | const cmdArgs = ['/c', 'start', title, '/wait', 'cmd.exe', '/c', command] 121 | 122 | // merge environment variables into a copy of the process.env 123 | const env = extendObject(extendObject({}, process.env), envVars) 124 | 125 | const options = { 126 | cwd: dir, 127 | env: env, 128 | windowsVerbatimArguments: true, 129 | } 130 | 131 | const cmd = CP.spawn(WindowsTerminalService.CMD, cmdArgs, options) 132 | cmd.on('error', reject) 133 | 134 | resolve(cmd) 135 | }) 136 | } 137 | 138 | public killTree(pid: number): Promise { 139 | // when killing a process in Windows its child processes are *not* killed but become root processes. 140 | // Therefore we use TASKKILL.EXE 141 | 142 | return new Promise((resolve, reject) => { 143 | const cmd = `taskkill /F /T /PID ${pid}` 144 | try { 145 | CP.execSync(cmd) 146 | resolve(undefined) 147 | } catch (err) { 148 | reject(err) 149 | } 150 | }) 151 | } 152 | } 153 | 154 | class LinuxTerminalService extends DefaultTerminalService { 155 | private static LINUX_TERM = '/usr/bin/gnome-terminal' // private const string LINUX_TERM = "/usr/bin/x-terminal-emulator"; 156 | private static WAIT_MESSAGE = 'Press any key to continue...' 157 | 158 | public launchInTerminal( 159 | dir: string, 160 | args: string[], 161 | envVars: { [key: string]: string } 162 | ): Promise { 163 | return new Promise((resolve, reject) => { 164 | if (!FS.existsSync(LinuxTerminalService.LINUX_TERM)) { 165 | reject( 166 | new Error( 167 | `Cannot find '${LinuxTerminalService.LINUX_TERM}' for launching the node program. See http://go.microsoft.com/fwlink/?linkID=534832#_20002` 168 | ) 169 | ) 170 | return 171 | } 172 | 173 | const bashCommand = `cd "${dir}"; "${args.join('" "')}"; echo; read -p "${ 174 | LinuxTerminalService.WAIT_MESSAGE 175 | }" -n1;` 176 | 177 | const termArgs = [ 178 | '--title', 179 | `"${LinuxTerminalService.TERMINAL_TITLE}"`, 180 | '-x', 181 | 'bash', 182 | '-c', 183 | `''${bashCommand}''`, // wrapping argument in two sets of ' because node is so "friendly" that it removes one set... 184 | ] 185 | 186 | // merge environment variables into a copy of the process.env 187 | const env = extendObject(extendObject({}, process.env), envVars) 188 | 189 | const options = { 190 | env: env, 191 | } 192 | 193 | const cmd = CP.spawn(LinuxTerminalService.LINUX_TERM, termArgs, options) 194 | cmd.on('error', reject) 195 | cmd.on('exit', (code: number) => { 196 | if (code === 0) { 197 | // OK 198 | resolve(undefined) // since cmd is not the terminal process but just a launcher, we do not pass it in the resolve to the caller 199 | } else { 200 | reject(new Error(`exit code: ${code}`)) 201 | } 202 | }) 203 | }) 204 | } 205 | } 206 | 207 | class MacTerminalService extends DefaultTerminalService { 208 | private static OSASCRIPT = '/usr/bin/osascript' // osascript is the AppleScript interpreter on OS X 209 | 210 | public launchInTerminal( 211 | dir: string, 212 | args: string[], 213 | envVars: { [key: string]: string } 214 | ): Promise { 215 | return new Promise((resolve, reject) => { 216 | // first fix the PATH so that 'runtimePath' can be found if installed with 'brew' 217 | // Utilities.FixPathOnOSX(); 218 | 219 | // On OS X we do not launch the program directly but we launch an AppleScript that creates (or reuses) a Terminal window 220 | // and then launches the program inside that window. 221 | 222 | const osaArgs = [ 223 | Path.join(__dirname, './TerminalHelper.scpt'), 224 | '-t', 225 | MacTerminalService.TERMINAL_TITLE, 226 | '-w', 227 | dir, 228 | ] 229 | 230 | for (const a of args) { 231 | osaArgs.push('-pa') 232 | osaArgs.push(a) 233 | } 234 | 235 | if (envVars) { 236 | for (const key in envVars) { 237 | osaArgs.push('-e') 238 | osaArgs.push(key + '=' + envVars[key]) 239 | } 240 | } 241 | 242 | let stderr = '' 243 | const osa = CP.spawn(MacTerminalService.OSASCRIPT, osaArgs) 244 | osa.on('error', reject) 245 | osa.stderr.on('data', (data: Buffer) => { 246 | stderr += data.toString() 247 | }) 248 | osa.on('exit', (code: number) => { 249 | if (code === 0) { 250 | // OK 251 | resolve(undefined) // since cmd is not the terminal process but just the osa tool, we do not pass it in the resolve to the caller 252 | } else { 253 | if (stderr) { 254 | reject(new Error(stderr)) 255 | } else { 256 | reject(new Error(`exit code: ${code}`)) 257 | } 258 | } 259 | }) 260 | }) 261 | } 262 | } 263 | 264 | // ---- private utilities ---- 265 | 266 | function extendObject(objectCopy: T, object: T): T { 267 | return { ...objectCopy, ...object } 268 | } 269 | -------------------------------------------------------------------------------- /src/terminateProcess.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | terminateTree() { 4 | for cpid in $(pgrep -P $1); do 5 | terminateTree $cpid 6 | done 7 | kill -9 $1 > /dev/null 2>&1 8 | } 9 | 10 | for pid in $*; do 11 | terminateTree $pid 12 | done 13 | -------------------------------------------------------------------------------- /src/test/cloud.ts: -------------------------------------------------------------------------------- 1 | import { XdebugCloudConnection } from '../cloud' 2 | import { Socket } from 'net' 3 | import { describe, it, beforeEach } from 'mocha' 4 | import * as Mocha from 'mocha' 5 | import { encode } from 'iconv-lite' 6 | import { ENCODING } from '../dbgp' 7 | 8 | describe('XdebugCloudConnection', () => { 9 | function _xmlCloud(cmd: string, success: number, msg = '', id = ''): Buffer { 10 | let err = `` 11 | if (!success) { 12 | err += `${msg}` 13 | } 14 | err += `` 15 | return _xml(err) 16 | } 17 | function _xml(xml: string): Buffer { 18 | const data = encode(`\n${xml}`, ENCODING) 19 | return Buffer.concat([encode(data.length.toString() + '\0', ENCODING), data, encode('\0', ENCODING)]) 20 | } 21 | 22 | // 23 | 24 | // user.name@xdebug.org 25 | 26 | // Cannot find account for 'test' 27 | 28 | // 29 | 30 | let conn: XdebugCloudConnection 31 | let testSocket: Socket 32 | 33 | beforeEach(() => { 34 | testSocket = new Socket() 35 | testSocket.connect = (...param): Socket => { 36 | if (param[1] instanceof Function) { 37 | testSocket.once('connect', param[1] as () => void) 38 | } 39 | return testSocket 40 | } 41 | testSocket.write = (...param): boolean => { 42 | setTimeout(() => { 43 | if (param[1] instanceof Function) { 44 | ;(param[1] as () => void)() 45 | } 46 | testSocket.emit('write', param[0]) 47 | }, 1) 48 | return true 49 | } 50 | testSocket.end = (...param): Socket => { 51 | setTimeout(() => { 52 | if (param[0] instanceof Function) { 53 | ;(param[0] as () => void)() 54 | } 55 | }, 1) 56 | return testSocket 57 | } 58 | conn = new XdebugCloudConnection('test', testSocket) 59 | }) 60 | 61 | it('should connect and stop', (done: Mocha.Done) => { 62 | testSocket.on('write', (buffer: string | Buffer) => { 63 | testSocket.emit('data', _xmlCloud('stop', 1)) 64 | }) 65 | conn.connectAndStop().then(done, done) 66 | testSocket.emit('connect') 67 | }) 68 | 69 | it('should connect and stop and fail', (done: Mocha.Done) => { 70 | testSocket.on('write', (buffer: string | Buffer) => { 71 | testSocket.emit( 72 | 'data', 73 | _xmlCloud('stop', 0, 'A client for test has not been previously registered', 'ERR-10') 74 | ) 75 | }) 76 | conn.connectAndStop().then( 77 | () => done(Error('should not have succeeded')), 78 | err => done() 79 | ) 80 | testSocket.emit('connect') 81 | }) 82 | 83 | it('should connect with error', (done: Mocha.Done) => { 84 | conn.connect().then( 85 | () => done(Error('should not have succeeded')), 86 | err => done() 87 | ) 88 | testSocket.emit('error', new Error('connection error')) 89 | }) 90 | 91 | it('should connect', (done: Mocha.Done) => { 92 | testSocket.on('write', (buffer: string | Buffer) => { 93 | testSocket.emit('data', _xmlCloud('init', 1)) 94 | }) 95 | conn.connect().then(done, done) 96 | testSocket.emit('connect') 97 | }) 98 | 99 | it('should connect and fail', (done: Mocha.Done) => { 100 | testSocket.on('write', (buffer: string | Buffer) => { 101 | testSocket.emit('data', _xmlCloud('init', 0, 'Cannot find account for test', 'CLOUD-ERR-03')) 102 | }) 103 | conn.connect().then( 104 | () => done(Error('should not have succeeded ')), 105 | err => done() 106 | ) 107 | testSocket.emit('connect') 108 | }) 109 | 110 | it('should connect and init', (done: Mocha.Done) => { 111 | testSocket.on('write', (buffer: string | Buffer) => { 112 | testSocket.emit('data', _xmlCloud('init', 1)) 113 | }) 114 | conn.connect().then(() => { 115 | // after connect, send init and wait for connection event 116 | testSocket.emit( 117 | 'data', 118 | _xml( 119 | '' 120 | ) 121 | ) 122 | }, done) 123 | conn.on('connection', conn => done()) 124 | testSocket.emit('connect') 125 | }) 126 | 127 | it('should connect and init and stop', (done: Mocha.Done) => { 128 | testSocket.once('write', (buffer: string | Buffer) => { 129 | testSocket.emit('data', _xmlCloud('init', 1)) 130 | }) 131 | conn.connect().then(() => { 132 | // after connect, send init and wait for connection event 133 | testSocket.emit( 134 | 'data', 135 | _xml( 136 | '' 137 | ) 138 | ) 139 | }, done) 140 | conn.on('connection', conn => { 141 | testSocket.once('write', (buffer: string | Buffer) => { 142 | testSocket.emit( 143 | 'data', 144 | _xml( 145 | '' 146 | ) 147 | ) 148 | }) 149 | conn.sendStopCommand().then(() => done(), done) 150 | }) 151 | testSocket.emit('connect') 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /src/test/dbgp.ts: -------------------------------------------------------------------------------- 1 | import { DbgpConnection, ENCODING } from '../dbgp' 2 | import { Socket } from 'net' 3 | import * as iconv from 'iconv-lite' 4 | import { assert } from 'chai' 5 | import { describe, it, beforeEach } from 'mocha' 6 | 7 | describe('DbgpConnection', () => { 8 | function makePacket(message: string): Buffer { 9 | const messageBuffer = iconv.encode(message, ENCODING) 10 | return Buffer.concat([Buffer.from(`${messageBuffer.length}\0`), messageBuffer, Buffer.from('\0')]) 11 | } 12 | 13 | const message = 14 | '\nThis is just a test' 15 | const packet = makePacket(message) 16 | 17 | let socket: Socket 18 | let conn: DbgpConnection 19 | beforeEach(() => { 20 | socket = new Socket() 21 | conn = new DbgpConnection(socket) 22 | }) 23 | 24 | it('should parse a response in one data event', done => { 25 | conn.on('message', (document: XMLDocument) => { 26 | assert.equal(document.documentElement.nodeName, 'init') 27 | assert.equal(document.documentElement.textContent, 'This is just a test') 28 | done() 29 | }) 30 | conn.on('warning', done) 31 | conn.on('error', done) 32 | setTimeout(() => { 33 | socket.emit('data', packet) 34 | }, 100) 35 | }) 36 | 37 | it('should parse a response over multiple data events', done => { 38 | conn.on('message', (document: XMLDocument) => { 39 | assert.equal(document.documentElement.nodeName, 'init') 40 | assert.equal(document.documentElement.textContent, 'This is just a test') 41 | done() 42 | }) 43 | conn.on('warning', done) 44 | conn.on('error', done) 45 | const part1 = packet.slice(0, 50) 46 | const part2 = packet.slice(50, 100) 47 | const part3 = packet.slice(100) 48 | setTimeout(() => { 49 | socket.emit('data', part1) 50 | setTimeout(() => { 51 | socket.emit('data', part2) 52 | setTimeout(() => { 53 | socket.emit('data', part3) 54 | }, 100) 55 | }, 100) 56 | }, 100) 57 | }) 58 | 59 | it('should parse multiple responses in one data event', done => { 60 | conn.once('message', (document: XMLDocument) => { 61 | assert.equal(document.documentElement.nodeName, 'init') 62 | assert.equal(document.documentElement.textContent, 'This is just a test') 63 | conn.once('message', (document: XMLDocument) => { 64 | assert.equal(document.documentElement.nodeName, 'response') 65 | assert.equal(document.documentElement.textContent, 'This is just another test') 66 | done() 67 | }) 68 | }) 69 | conn.on('warning', done) 70 | conn.on('error', done) 71 | const packet2 = makePacket( 72 | '\nThis is just another test' 73 | ) 74 | setTimeout(() => { 75 | socket.emit('data', packet) 76 | setTimeout(() => { 77 | socket.emit('data', packet2) 78 | }) 79 | }, 100) 80 | }) 81 | 82 | it('should error on invalid XML', () => 83 | new Promise((resolve, reject) => { 84 | conn.on('error', (error: Error) => { 85 | assert.isDefined(error) 86 | assert.instanceOf(error, Error) 87 | resolve() 88 | }) 89 | conn.once('message', (document: XMLDocument) => { 90 | reject(new Error('emitted message event')) 91 | }) 92 | socket.emit('data', makePacket('<<>>><>')) 93 | })) 94 | }) 95 | -------------------------------------------------------------------------------- /src/test/envfile.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { describe, it } from 'mocha' 3 | import { getConfiguredEnvironment } from '../envfile' 4 | 5 | describe('EnvFile', () => { 6 | it('should work without envfile', () => { 7 | const ret = getConfiguredEnvironment({ env: { TEST: 'TEST' } }) 8 | assert.deepEqual(ret, { TEST: 'TEST' }) 9 | }) 10 | it('should work with missing envfile', () => { 11 | const ret = getConfiguredEnvironment({ env: { TEST: 'TEST' }, envFile: 'NONEXISTINGFILE' }) 12 | assert.deepEqual(ret, { TEST: 'TEST' }) 13 | }) 14 | it('should merge envfile', () => { 15 | const ret = getConfiguredEnvironment({ env: { TEST: 'TEST' }, envFile: 'testproject/envfile' }) 16 | assert.deepEqual(ret, { TEST: 'TEST', TEST1: 'VALUE1', Test2: 'Value2' }) 17 | }) 18 | ;(process.platform === 'win32' ? it : it.skip)('should merge envfile on win32', () => { 19 | const ret = getConfiguredEnvironment({ env: { TEST1: 'TEST' }, envFile: 'testproject/envfile' }) 20 | assert.deepEqual(ret, { TEST1: 'TEST', Test2: 'Value2' }) 21 | }) 22 | ;(process.platform === 'win32' ? it : it.skip)('should merge envfile on win32 case insensitive', () => { 23 | const ret = getConfiguredEnvironment({ env: { Test1: 'TEST' }, envFile: 'testproject/envfile' }) 24 | assert.deepEqual(ret, { TEST1: 'TEST', Test2: 'Value2' }) 25 | }) 26 | ;(process.platform !== 'win32' ? it : it.skip)('should merge envfile on unix', () => { 27 | const ret = getConfiguredEnvironment({ env: { TEST1: 'TEST' }, envFile: 'testproject/envfile' }) 28 | assert.deepEqual(ret, { TEST1: 'TEST', Test2: 'Value2' }) 29 | }) 30 | ;(process.platform !== 'win32' ? it : it.skip)('should merge envfile on unix case insensitive', () => { 31 | const ret = getConfiguredEnvironment({ env: { Test1: 'TEST' }, envFile: 'testproject/envfile' }) 32 | assert.deepEqual(ret, { Test1: 'TEST', TEST1: 'VALUE1', Test2: 'Value2' }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/test/ignore.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { describe, it } from 'mocha' 3 | import { shouldIgnoreException } from '../ignore' 4 | 5 | describe('ignoreExceptions', () => { 6 | it('should match exact', () => { 7 | assert.isTrue(shouldIgnoreException('BaseException', ['BaseException'])) 8 | }) 9 | it('should no match exact', () => { 10 | assert.isFalse(shouldIgnoreException('BaseException', ['SomeOtherException'])) 11 | }) 12 | it('should match wildcard end exact', () => { 13 | assert.isTrue(shouldIgnoreException('BaseException', ['BaseException*'])) 14 | }) 15 | it('should match wildcard end extra', () => { 16 | assert.isTrue(shouldIgnoreException('BaseExceptionMore', ['BaseException*'])) 17 | }) 18 | it('should match namespaced exact', () => { 19 | assert.isTrue(shouldIgnoreException('NS1\\BaseException', ['NS1\\BaseException'])) 20 | }) 21 | it('should match namespaced wildcard exact', () => { 22 | assert.isTrue(shouldIgnoreException('NS1\\BaseException', ['NS1\\BaseException*'])) 23 | }) 24 | it('should match namespaced wildcard extra', () => { 25 | assert.isTrue(shouldIgnoreException('NS1\\BaseExceptionMore', ['NS1\\BaseException*'])) 26 | }) 27 | it('should match namespaced wildcard whole level', () => { 28 | assert.isTrue(shouldIgnoreException('NS1\\BaseException', ['NS1\\*'])) 29 | }) 30 | it('should not match namespaced wildcard more levels', () => { 31 | assert.isFalse(shouldIgnoreException('NS1\\NS2\\BaseException', ['NS1\\*'])) 32 | }) 33 | it('should match namespaced wildcard in middle', () => { 34 | assert.isTrue(shouldIgnoreException('NS1\\NS2\\BaseException', ['NS1\\*\\BaseException'])) 35 | }) 36 | it('should match namespaced wildcard multiple', () => { 37 | assert.isTrue(shouldIgnoreException('NS1\\NS2\\NS3\\BaseException', ['NS1\\*\\*\\BaseException'])) 38 | }) 39 | it('should match namespaced wildcard levels', () => { 40 | assert.isTrue(shouldIgnoreException('NS1\\NS2\\NS3\\BaseException', ['NS1\\**\\BaseException'])) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/test/logpoint.ts: -------------------------------------------------------------------------------- 1 | import { LogPointManager } from '../logpoint' 2 | import * as assert from 'assert' 3 | import { describe, it, beforeEach } from 'mocha' 4 | 5 | describe('logpoint', () => { 6 | const FILE_URI1 = 'file://my/file1' 7 | const FILE_URI2 = 'file://my/file2' 8 | const FILE_URI3 = 'file://my/file3' 9 | 10 | const LOG_MESSAGE_VAR = '{$variable1}' 11 | const LOG_MESSAGE_MULTIPLE = '{$variable1} {$variable3} {$variable2}' 12 | const LOG_MESSAGE_TEXT_AND_VAR = 'This is my {$variable1}' 13 | const LOG_MESSAGE_TEXT_AND_MULTIVAR = 'Those variables: {$variable1} ${$variable2} should be replaced' 14 | const LOG_MESSAGE_REPEATED_VAR = 'This {$variable1} and {$variable1} should be equal' 15 | const LOG_MESSAGE_BADLY_FORMATED_VAR = 'Only {$variable1} should be resolved and not }$variable1 and $variable1{}' 16 | 17 | const REPLACE_FUNCTION = (str: string): Promise => { 18 | return Promise.resolve(`${str}_value`) 19 | } 20 | 21 | let logPointManager: LogPointManager 22 | 23 | beforeEach('create new instance', () => (logPointManager = new LogPointManager())) 24 | 25 | describe('basic map management', () => { 26 | it('should contain added logpoints', () => { 27 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR) 28 | logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR) 29 | logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR) 30 | logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR) 31 | 32 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true) 33 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true) 34 | assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true) 35 | assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true) 36 | 37 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 12), false) 38 | assert.equal(logPointManager.hasLogPoint(FILE_URI2, 13), false) 39 | assert.equal(logPointManager.hasLogPoint(FILE_URI3, 10), false) 40 | }) 41 | 42 | it('should add and clear entries', () => { 43 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR) 44 | logPointManager.addLogPoint(FILE_URI1, 11, LOG_MESSAGE_VAR) 45 | logPointManager.addLogPoint(FILE_URI2, 12, LOG_MESSAGE_VAR) 46 | logPointManager.addLogPoint(FILE_URI3, 13, LOG_MESSAGE_VAR) 47 | 48 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), true) 49 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), true) 50 | assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true) 51 | assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true) 52 | 53 | logPointManager.clearFromFile(FILE_URI1) 54 | 55 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 10), false) 56 | assert.equal(logPointManager.hasLogPoint(FILE_URI1, 11), false) 57 | assert.equal(logPointManager.hasLogPoint(FILE_URI2, 12), true) 58 | assert.equal(logPointManager.hasLogPoint(FILE_URI3, 13), true) 59 | }) 60 | }) 61 | 62 | describe('variable resolution', () => { 63 | it('should resolve variables', async () => { 64 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_VAR) 65 | const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) 66 | assert.equal(result, '$variable1_value') 67 | }) 68 | 69 | it('should resolve multiple variables', async () => { 70 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_MULTIPLE) 71 | const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) 72 | assert.equal(result, '$variable1_value $variable3_value $variable2_value') 73 | }) 74 | 75 | it('should resolve variables with text', async () => { 76 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_VAR) 77 | const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) 78 | assert.equal(result, 'This is my $variable1_value') 79 | }) 80 | 81 | it('should resolve multiple variables with text', async () => { 82 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_TEXT_AND_MULTIVAR) 83 | const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) 84 | assert.equal(result, 'Those variables: $variable1_value $$variable2_value should be replaced') 85 | }) 86 | 87 | it('should resolve repeated variables', async () => { 88 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_REPEATED_VAR) 89 | const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) 90 | assert.equal(result, 'This $variable1_value and $variable1_value should be equal') 91 | }) 92 | 93 | it('should resolve repeated bad formated messages correctly', async () => { 94 | logPointManager.addLogPoint(FILE_URI1, 10, LOG_MESSAGE_BADLY_FORMATED_VAR) 95 | const result = await logPointManager.resolveExpressions(FILE_URI1, 10, REPLACE_FUNCTION) 96 | assert.equal(result, 'Only $variable1_value should be resolved and not }$variable1 and $variable1') 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /src/test/paths.ts: -------------------------------------------------------------------------------- 1 | import { isSameUri, convertClientPathToDebugger, convertDebuggerPathToClient, isPositiveMatchInGlobs } from '../paths' 2 | import * as assert from 'assert' 3 | import { describe, it } from 'mocha' 4 | 5 | describe('paths', () => { 6 | describe('isSameUri', () => { 7 | it('should compare to URIs', () => { 8 | assert.strictEqual(isSameUri('file:///var/www/test.php', 'file:///var/www/test.php'), true) 9 | assert.strictEqual(isSameUri('file:///var/www/test.php', 'file:///var/www/test2.php'), false) 10 | }) 11 | it('should compare windows paths case-insensitive', () => { 12 | assert.strictEqual( 13 | isSameUri( 14 | 'file:///C:/Program%20Files/Apache/2.4/htdocs/test.php', 15 | 'file:///c:/Program%20Files/Apache/2.4/htdocs/test.php' 16 | ), 17 | true 18 | ) 19 | assert.strictEqual( 20 | isSameUri( 21 | 'file:///C:/Program%20Files/Apache/2.4/htdocs/test.php', 22 | 'file:///C:/Program%20Files/Apache/2.4/htdocs/test2.php' 23 | ), 24 | false 25 | ) 26 | }) 27 | }) 28 | describe('convertClientPathToDebugger', () => { 29 | describe('without source mapping', () => { 30 | it('should convert a windows path to a URI', () => { 31 | assert.equal( 32 | convertClientPathToDebugger('C:\\Users\\felix\\test.php'), 33 | 'file:///C:/Users/felix/test.php' 34 | ) 35 | }) 36 | it('should convert a unix path to a URI', () => { 37 | assert.equal(convertClientPathToDebugger('/home/felix/test.php'), 'file:///home/felix/test.php') 38 | }) 39 | }) 40 | describe('with source mapping', () => { 41 | // unix to unix 42 | it('should convert a unix path to a unix URI', () => { 43 | // site 44 | assert.equal( 45 | convertClientPathToDebugger('/home/felix/mysite/site.php', { 46 | '/var/www': '/home/felix/mysite', 47 | '/app': '/home/felix/mysource', 48 | }), 49 | 'file:///var/www/site.php' 50 | ) 51 | // source 52 | assert.equal( 53 | convertClientPathToDebugger('/home/felix/mysource/source.php', { 54 | '/var/www': '/home/felix/mysite', 55 | '/app': '/home/felix/mysource', 56 | }), 57 | 'file:///app/source.php' 58 | ) 59 | // longest prefix matching for server paths 60 | assert.strictEqual( 61 | convertClientPathToDebugger('/home/felix/mysource/subdir/source.php', { 62 | '/var/www': '/home/felix/mysite', 63 | '/app/subdir1': '/home/felix/mysource/subdir', 64 | '/app': '/home/felix/mysource', 65 | }), 66 | 'file:///app/subdir1/source.php' 67 | ) 68 | }) 69 | // unix to windows 70 | it('should convert a unix path to a windows URI', () => { 71 | // site 72 | assert.equal( 73 | convertClientPathToDebugger('/home/felix/mysite/site.php', { 74 | 'C:\\Program Files\\Apache\\2.4\\htdocs': '/home/felix/mysite', 75 | 'C:\\Program Files\\MySource': '/home/felix/mysource', 76 | }), 77 | 'file:///C:/Program%20Files/Apache/2.4/htdocs/site.php' 78 | ) 79 | // source 80 | assert.equal( 81 | convertClientPathToDebugger('/home/felix/mysource/source.php', { 82 | 'C:\\Program Files\\Apache\\2.4\\htdocs': '/home/felix/mysite', 83 | 'C:\\Program Files\\MySource': '/home/felix/mysource', 84 | }), 85 | 'file:///C:/Program%20Files/MySource/source.php' 86 | ) 87 | }) 88 | // windows to unix 89 | it('should convert a windows path to a unix URI', () => { 90 | // site 91 | assert.equal( 92 | convertClientPathToDebugger('C:\\Users\\felix\\mysite\\site.php', { 93 | '/var/www': 'C:\\Users\\felix\\mysite', 94 | '/app': 'C:\\Users\\felix\\mysource', 95 | }), 96 | 'file:///var/www/site.php' 97 | ) 98 | // source 99 | assert.equal( 100 | convertClientPathToDebugger('C:\\Users\\felix\\mysource\\source.php', { 101 | '/var/www': 'C:\\Users\\felix\\mysite', 102 | '/app': 'C:\\Users\\felix\\mysource', 103 | }), 104 | 'file:///app/source.php' 105 | ) 106 | // only driv eletter 107 | assert.equal( 108 | convertClientPathToDebugger('C:\\source.php', { 109 | '/var/www': 'C:', 110 | }), 111 | 'file:///var/www/source.php' 112 | ) 113 | // only driv eletter 114 | assert.equal( 115 | convertClientPathToDebugger('C:\\app\\source.php', { 116 | '/': 'C:', 117 | }), 118 | 'file:///app/source.php' 119 | ) 120 | // drive letter with slash 121 | assert.equal( 122 | convertClientPathToDebugger('C:\\app\\source.php', { 123 | '/var/www': 'C:/', 124 | }), 125 | 'file:///var/www/app/source.php' 126 | ) 127 | // drive letter with slash 128 | assert.equal( 129 | convertClientPathToDebugger('C:\\app\\source.php', { 130 | '/': 'C:/', 131 | }), 132 | 'file:///app/source.php' 133 | ) 134 | }) 135 | it('should convert a windows path with inconsistent casing to a unix URI', () => { 136 | const localSourceRoot = 'C:\\Users\\felix\\myproject' 137 | const serverSourceRoot = '/var/www' 138 | assert.equal( 139 | convertClientPathToDebugger('c:\\Users\\felix\\myproject\\test.php', { 140 | [serverSourceRoot]: localSourceRoot, 141 | }), 142 | 'file:///var/www/test.php' 143 | ) 144 | }) 145 | // windows to windows 146 | it('should convert a windows path to a windows URI', () => { 147 | // site 148 | assert.equal( 149 | convertClientPathToDebugger('C:\\Users\\felix\\mysite\\site.php', { 150 | 'C:\\Program Files\\Apache\\2.4\\htdocs': 'C:\\Users\\felix\\mysite', 151 | 'C:\\Program Files\\MySource': 'C:\\Users\\felix\\mysource', 152 | }), 153 | 'file:///C:/Program%20Files/Apache/2.4/htdocs/site.php' 154 | ) 155 | // source 156 | assert.equal( 157 | convertClientPathToDebugger('C:\\Users\\felix\\mysource\\source.php', { 158 | 'C:\\Program Files\\Apache\\2.4\\htdocs': 'C:\\Users\\felix\\mysite', 159 | 'C:\\Program Files\\MySource': 'C:\\Users\\felix\\mysource', 160 | }), 161 | 'file:///C:/Program%20Files/MySource/source.php' 162 | ) 163 | }) 164 | }) 165 | describe('exact file mappings', () => { 166 | it('should map exact unix path', () => { 167 | assert.equal( 168 | convertClientPathToDebugger('/var/path/file.php', { 169 | '/var/path2/file2.php': '/var/path/file.php', 170 | }), 171 | 'file:///var/path2/file2.php' 172 | ) 173 | }) 174 | it('should map exact windows path', () => { 175 | assert.equal( 176 | convertClientPathToDebugger('C:\\var\\path\\file.php', { 177 | 'C:\\var\\path2\\file2.php': 'C:\\var\\path\\file.php', 178 | }), 179 | 'file:///C:/var/path2/file2.php' 180 | ) 181 | }) 182 | }) 183 | describe('relative paths', () => { 184 | it('should resolve relative path posix', () => { 185 | assert.equal(convertClientPathToDebugger('/var/www/foo/../bar'), 'file:///var/www/bar') 186 | }) 187 | it('should resolve relative path maps posix', () => { 188 | assert.equal( 189 | convertClientPathToDebugger('/work/foo/test.php', { 190 | '/var/www/html/bar': '/work/project/folder/../../foo', 191 | }), 192 | 'file:///var/www/html/bar/test.php' 193 | ) 194 | }) 195 | it('should resolve relative path win32', () => { 196 | assert.equal(convertClientPathToDebugger('C:\\var\\www\\foo\\..\\bar'), 'file:///C:/var/www/bar') 197 | }) 198 | it('should resolve relative path maps win32 to posix', () => { 199 | assert.equal( 200 | convertClientPathToDebugger('C:\\work\\foo\\test.php', { 201 | '/var/www/html/bar': 'C:\\work\\project\\folder\\..\\..\\foo', 202 | }), 203 | 'file:///var/www/html/bar/test.php' 204 | ) 205 | }) 206 | it('should resolve relative path maps win32 to win32', () => { 207 | assert.equal( 208 | convertClientPathToDebugger('C:\\work\\foo\\test.php', { 209 | 'C:\\var\\www\\html\\bar': 'C:\\work\\project\\folder\\..\\..\\foo', 210 | }), 211 | 'file:///C:/var/www/html/bar/test.php' 212 | ) 213 | }) 214 | }) 215 | }) 216 | describe('convertDebuggerPathToClient', () => { 217 | describe('without source mapping', () => { 218 | it('should convert a windows URI to a windows path', () => { 219 | assert.equal( 220 | convertDebuggerPathToClient('file:///C:/Users/felix/test.php'), 221 | 'C:\\Users\\felix\\test.php' 222 | ) 223 | }) 224 | it('should convert a unix URI to a unix path', () => { 225 | assert.equal(convertDebuggerPathToClient('file:///home/felix/test.php'), '/home/felix/test.php') 226 | }) 227 | it('should handle non-unicode special characters', () => { 228 | assert.equal( 229 | convertDebuggerPathToClient('file:///d:/arx%20iT/2-R%C3%A9alisation/mmi/V1.0/Web/core/header.php'), 230 | 'd:\\arx iT\\2-Réalisation\\mmi\\V1.0\\Web\\core\\header.php' 231 | ) 232 | }) 233 | }) 234 | describe('with source mapping', () => { 235 | // unix to unix 236 | it('should map unix uris to unix paths', () => { 237 | // site 238 | assert.equal( 239 | convertDebuggerPathToClient('file:///var/www/site.php', { 240 | '/var/www': '/home/felix/mysite', 241 | '/app': '/home/felix/mysource', 242 | }), 243 | '/home/felix/mysite/site.php' 244 | ) 245 | // source 246 | assert.equal( 247 | convertDebuggerPathToClient('file:///app/source.php', { 248 | '/var/www': '/home/felix/mysite', 249 | '/app': '/home/felix/mysource', 250 | }), 251 | '/home/felix/mysource/source.php' 252 | ) 253 | // longest prefix matching for local paths 254 | assert.strictEqual( 255 | convertDebuggerPathToClient('file:///app/subdir/source.php', { 256 | '/var/www': '/home/felix/mysite', 257 | '/app/subdir': '/home/felix/mysource/subdir1', 258 | '/app': '/home/felix/mysource', 259 | }), 260 | '/home/felix/mysource/subdir1/source.php' 261 | ) 262 | }) 263 | // unix to windows 264 | it('should map unix uris to windows paths', () => { 265 | // site 266 | assert.equal( 267 | convertDebuggerPathToClient('file:///var/www/site.php', { 268 | '/var/www': 'C:\\Users\\felix\\mysite', 269 | '/app': 'C:\\Users\\felix\\mysource', 270 | }), 271 | 'C:\\Users\\felix\\mysite\\site.php' 272 | ) 273 | // source 274 | assert.equal( 275 | convertDebuggerPathToClient('file:///app/source.php', { 276 | '/var/www': 'C:\\Users\\felix\\mysite', 277 | '/app': 'C:\\Users\\felix\\mysource', 278 | }), 279 | 'C:\\Users\\felix\\mysource\\source.php' 280 | ) 281 | // only drive letter 282 | assert.equal( 283 | convertDebuggerPathToClient('file:///var/www/source.php', { 284 | '/var/www': 'C:', 285 | }), 286 | 'C:\\source.php' 287 | ) 288 | // only drive letter 289 | assert.equal( 290 | convertDebuggerPathToClient('file:///app/source.php', { 291 | '/': 'C:', 292 | }), 293 | 'C:\\app\\source.php' 294 | ) 295 | // drive letter with slash 296 | assert.equal( 297 | convertDebuggerPathToClient('file:///var/www/source.php', { 298 | '/var': 'C:/', 299 | }), 300 | 'C:\\www\\source.php' 301 | ) 302 | // drive letter with slash 303 | assert.equal( 304 | convertDebuggerPathToClient('file:///app/source.php', { 305 | '/': 'C:/', 306 | }), 307 | 'C:\\app\\source.php' 308 | ) 309 | }) 310 | // windows to unix 311 | it('should map windows uris to unix paths', () => { 312 | // dir/site 313 | assert.equal( 314 | convertDebuggerPathToClient('file:///C:/Program%20Files/Apache/2.4/htdocs/dir/site.php', { 315 | 'C:\\Program Files\\Apache\\2.4\\htdocs': '/home/felix/mysite', 316 | 'C:\\Program Files\\MySource': '/home/felix/mysource', 317 | }), 318 | '/home/felix/mysite/dir/site.php' 319 | ) 320 | // site 321 | assert.equal( 322 | convertDebuggerPathToClient('file:///C:/Program%20Files/Apache/2.4/htdocs/site.php', { 323 | 'C:\\Program Files\\Apache\\2.4\\htdocs': '/home/felix/mysite', 324 | 'C:\\Program Files\\MySource': '/home/felix/mysource', 325 | }), 326 | '/home/felix/mysite/site.php' 327 | ) 328 | // source 329 | assert.equal( 330 | convertDebuggerPathToClient('file:///C:/Program%20Files/MySource/source.php', { 331 | 'C:\\Program Files\\Apache\\2.4\\htdocs': '/home/felix/mysite', 332 | 'C:\\Program Files\\MySource': '/home/felix/mysource', 333 | }), 334 | '/home/felix/mysource/source.php' 335 | ) 336 | // multi level source 337 | assert.equal( 338 | convertDebuggerPathToClient('file:///C:/Program%20Files/MySource/src/app/source.php', { 339 | 'C:\\Program Files\\Apache\\2.4\\htdocs': '/home/felix/mysite', 340 | 'C:\\Program Files\\MySource': '/home/felix/mysource', 341 | }), 342 | '/home/felix/mysource/src/app/source.php' 343 | ) 344 | }) 345 | // windows to windows 346 | it('should map windows uris to windows paths', () => { 347 | // site 348 | assert.equal( 349 | convertDebuggerPathToClient('file:///C:/Program%20Files/Apache/2.4/htdocs/site.php', { 350 | 'C:\\Program Files\\Apache\\2.4\\htdocs': 'C:\\Users\\felix\\mysite', 351 | 'C:\\Program Files\\MySource': 'C:\\Users\\felix\\mysource', 352 | }), 353 | 'C:\\Users\\felix\\mysite\\site.php' 354 | ) 355 | // source 356 | assert.equal( 357 | convertDebuggerPathToClient('file:///C:/Program%20Files/MySource/source.php', { 358 | 'C:\\Program Files\\Apache\\2.4\\htdocs': 'C:\\Users\\felix\\mysite', 359 | 'C:\\Program Files\\MySource': 'C:\\Users\\felix\\mysource', 360 | }), 361 | 'C:\\Users\\felix\\mysource\\source.php' 362 | ) 363 | }) 364 | }) 365 | describe('exact file mappings', () => { 366 | it('should map exact unix path', () => { 367 | assert.equal( 368 | convertDebuggerPathToClient('file:///var/path2/file2.php', { 369 | '/var/path2/file2.php': '/var/path/file.php', 370 | }), 371 | '/var/path/file.php' 372 | ) 373 | }) 374 | it('should map exact windows path', () => { 375 | assert.equal( 376 | convertDebuggerPathToClient('file:///C:/var/path2/file2.php', { 377 | 'C:\\var\\path2\\file2.php': 'C:\\var\\path\\file.php', 378 | }), 379 | 'C:\\var\\path\\file.php' 380 | ) 381 | }) 382 | }) 383 | }) 384 | describe('sshfs', () => { 385 | it('should map sshfs to remote unix', () => { 386 | assert.equal( 387 | convertClientPathToDebugger('ssh://host/path/file.php', { 388 | '/root/path': 'ssh://host/path/', 389 | }), 390 | 'file:///root/path/file.php' 391 | ) 392 | }) 393 | it('should map remote unix to sshfs', () => { 394 | assert.equal( 395 | convertDebuggerPathToClient('file:///root/path/file.php', { 396 | '/root/path': 'ssh://host/path/', 397 | }), 398 | 'ssh://host/path/file.php' 399 | ) 400 | }) 401 | it('should map sshfs to remote unix relative', () => { 402 | assert.equal( 403 | convertClientPathToDebugger('ssh://host/path/file.php', { 404 | '/root/path': 'ssh://host/test/../path/', 405 | }), 406 | 'file:///root/path/file.php' 407 | ) 408 | }) 409 | it('should map remote unix to sshfs relative', () => { 410 | assert.equal( 411 | convertDebuggerPathToClient('file:///root/path/file.php', { 412 | '/root/path': 'ssh://host/test/../path/', 413 | }), 414 | 'ssh://host/path/file.php' 415 | ) 416 | }) 417 | }) 418 | describe('UNC', () => { 419 | it('should convert UNC to url', () => { 420 | assert.equal(convertClientPathToDebugger('\\\\DARKPAD\\smb\\test1.php', {}), 'file://darkpad/smb/test1.php') 421 | }) 422 | it('should convert url to UNC', () => { 423 | assert.equal(convertDebuggerPathToClient('file://DARKPAD/SMB/test2.php', {}), '\\\\darkpad\\SMB\\test2.php') 424 | }) 425 | }) 426 | describe('UNC mapping', () => { 427 | it('should convert UNC to mapped url', () => { 428 | assert.equal( 429 | convertClientPathToDebugger('\\\\DARKPAD\\smb\\test1.php', { 430 | '/var/test': '\\\\DARKPAD\\smb', 431 | }), 432 | 'file:///var/test/test1.php' 433 | ) 434 | }) 435 | it('should convert url to mapped UNC', () => { 436 | assert.equal( 437 | convertDebuggerPathToClient('file:///var/test/test2.php', { 438 | '/var/test': '\\\\DARKPAD\\smb', 439 | }), 440 | '\\\\darkpad\\smb\\test2.php' 441 | ) 442 | }) 443 | }) 444 | describe('Phar', () => { 445 | it('should map win32 Phar client to debugger', () => { 446 | assert.equal( 447 | convertClientPathToDebugger('C:\\otherfolder\\internal\\file.php', { 448 | 'phar://C:/folder/file.phar': 'C:\\otherfolder', 449 | }), 450 | 'phar://C:/folder/file.phar/internal/file.php' 451 | ) 452 | }) 453 | it('should map win32 Phar debugger to debugger', () => { 454 | assert.equal( 455 | convertDebuggerPathToClient('phar://C:/folder/file.phar/internal/file.php', { 456 | 'phar://C:/folder/file.phar': 'C:\\otherfolder', 457 | }), 458 | 'C:\\otherfolder\\internal\\file.php' 459 | ) 460 | }) 461 | it('should map posix Phar client to debugger', () => { 462 | assert.equal( 463 | convertClientPathToDebugger('/otherfolder/internal/file.php', { 464 | 'phar:///folder/file.phar': '/otherfolder', 465 | }), 466 | 'phar:///folder/file.phar/internal/file.php' 467 | ) 468 | }) 469 | it('should map posix Phar debugger to debugger', () => { 470 | assert.equal( 471 | convertDebuggerPathToClient('phar:///folder/file.phar/internal/file.php', { 472 | 'phar:///folder/file.phar': '/otherfolder', 473 | }), 474 | '/otherfolder/internal/file.php' 475 | ) 476 | }) 477 | }) 478 | describe('isPositiveMatchInGlobs', () => { 479 | it('should not match empty globs', () => { 480 | assert.equal(isPositiveMatchInGlobs('/test/test.php', []), false) 481 | }) 482 | it('should match positive globs', () => { 483 | assert.equal(isPositiveMatchInGlobs('/test/test.php', ['**/test/**']), true) 484 | }) 485 | it('should not match positive globs', () => { 486 | assert.equal(isPositiveMatchInGlobs('/test/test.php', ['**/not_test/**']), false) 487 | }) 488 | it('should match negative globs', () => { 489 | assert.equal(isPositiveMatchInGlobs('/test/test.php', ['!**/test.php', '**/test/**']), false) 490 | }) 491 | it('should not match negative globs', () => { 492 | assert.equal(isPositiveMatchInGlobs('/test/test.php', ['!**/not_test/test.php', '**/test/**']), true) 493 | }) 494 | }) 495 | }) 496 | -------------------------------------------------------------------------------- /src/test/proxy.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import { ProxyConnect, ProxyMessages } from '../proxyConnect' 3 | import { encode } from 'iconv-lite' 4 | import { ENCODING } from '../dbgp' 5 | import { Socket } from 'net' 6 | import { describe, it, beforeEach } from 'mocha' 7 | import * as Mocha from 'mocha' 8 | 9 | describe('ProxyConnect', () => { 10 | function _xml(cmd: string, success: number, msg = '', id = 0): Buffer { 11 | const err = `${msg}` 12 | return encode(`\n${err}`, ENCODING) 13 | } 14 | 15 | const host = 'host' 16 | const port = 9001 17 | let conn: ProxyConnect 18 | let testSocket: Socket 19 | let msgs: ProxyMessages 20 | 21 | function doneOnce(done: Mocha.Done): Mocha.Done { 22 | let fired = false 23 | return (err?: any) => { 24 | if (!fired) { 25 | fired = true 26 | done(err) 27 | } 28 | } 29 | } 30 | 31 | beforeEach(() => { 32 | testSocket = new Socket() 33 | testSocket.connect = (...param): Socket => { 34 | return testSocket 35 | } 36 | conn = new ProxyConnect(host, port, 9000, true, undefined, 3000, testSocket) 37 | msgs = conn.msgs 38 | }) 39 | 40 | it('should timeout', (done: Mocha.Done) => { 41 | assert.exists(conn) 42 | conn.sendProxyInitCommand().catch((err: Error) => { 43 | assert.equal(err.message, msgs.timeout) 44 | done() 45 | }) 46 | testSocket.emit('error', new Error(msgs.timeout)) 47 | }) 48 | 49 | it('should fail if proxy is unreachable', (done: Mocha.Done) => { 50 | assert.exists(conn) 51 | conn.sendProxyInitCommand().catch((err: Error) => { 52 | assert.equal(err.message, msgs.resolve) 53 | done() 54 | }) 55 | testSocket.emit('lookup', new Error(msgs.resolve)) 56 | }) 57 | 58 | it('should throw an error for duplicate IDE key', (done: Mocha.Done) => { 59 | assert.exists(conn) 60 | conn.sendProxyInitCommand().catch((err: Error) => { 61 | assert.equal(err.message, msgs.duplicateKey) 62 | done() 63 | }) 64 | 65 | testSocket.emit('data', _xml('init', 0, msgs.duplicateKey)) 66 | testSocket.emit('close', false) 67 | }) 68 | 69 | it('should request registration', (done: Mocha.Done) => { 70 | done = doneOnce(done) 71 | conn.on('log_request', (str: string) => { 72 | assert.equal(str, msgs.registerInfo) 73 | done() 74 | }) 75 | 76 | conn.sendProxyInitCommand().catch((err: Error) => { 77 | done(err) 78 | }) 79 | }) 80 | 81 | it('should be registered', (done: Mocha.Done) => { 82 | conn.on('log_response', (str: string) => { 83 | assert.equal(str, msgs.registerSuccess) 84 | done() 85 | }) 86 | 87 | conn.sendProxyInitCommand().catch((err: Error) => { 88 | done(err) 89 | }) 90 | testSocket.emit('data', _xml('init', 1)) 91 | testSocket.emit('close', false) 92 | }) 93 | 94 | it('should request deregistration', (done: Mocha.Done) => { 95 | done = doneOnce(done) 96 | conn.on('log_request', (str: string) => { 97 | assert.equal(str, msgs.deregisterInfo) 98 | done() 99 | }) 100 | testSocket.emit('data', _xml('init', 1)) 101 | testSocket.emit('close', false) 102 | 103 | conn.sendProxyStopCommand().catch((err: Error) => { 104 | done(err) 105 | }) 106 | }) 107 | 108 | it('should be deregistered', (done: Mocha.Done) => { 109 | conn.on('log_response', (str: string) => { 110 | assert.equal(str, msgs.deregisterSuccess) 111 | done() 112 | }) 113 | testSocket.emit('data', _xml('stop', 1)) 114 | testSocket.emit('close', false) 115 | conn.sendProxyStopCommand().catch((err: Error) => { 116 | done(err) 117 | }) 118 | }) 119 | 120 | it('should throw an error for nonexistent IDE key', (done: Mocha.Done) => { 121 | conn.sendProxyInitCommand().catch((err: Error) => { 122 | done(err) 123 | }) 124 | testSocket.emit('data', _xml('init', 1)) 125 | testSocket.emit('close', false) 126 | 127 | conn.sendProxyStopCommand().catch((err: Error) => { 128 | assert.equal(msgs.nonexistentKey, err.message) 129 | done() 130 | }) 131 | testSocket.emit('data', _xml('stop', 0, msgs.nonexistentKey)) 132 | testSocket.emit('close', false) 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /src/varExport.ts: -------------------------------------------------------------------------------- 1 | import * as xdebug from './xdebugConnection' 2 | 3 | export async function varExportProperty(property: xdebug.Property, indent: string = ''): Promise { 4 | if (indent.length >= 20) { 5 | // prevent infinite recursion 6 | return `...` 7 | } 8 | 9 | let displayValue: string 10 | if (property.hasChildren || property.type === 'array' || property.type === 'object') { 11 | if (!property.children || property.children.length === 0) { 12 | // TODO: also take into account the number of children for pagination 13 | property.children = await property.getChildren() 14 | } 15 | displayValue = ( 16 | await Promise.all( 17 | property.children.map(async property => { 18 | const indent2 = indent + ' ' 19 | if (property.hasChildren) { 20 | return `${indent2}${property.name} => \n${indent2}${await varExportProperty( 21 | property, 22 | indent2 23 | )},` 24 | } else { 25 | return `${indent2}${property.name} => ${await varExportProperty(property, indent2)},` 26 | } 27 | }) 28 | ) 29 | ).join('\n') 30 | 31 | if (property.type === 'array') { 32 | // for arrays, show the length, like a var_dump would do 33 | displayValue = `array (\n${displayValue}\n${indent})` 34 | } else if (property.type === 'object' && property.class) { 35 | // for objects, show the class name as type (if specified) 36 | displayValue = `${property.class}::__set_state(array(\n${displayValue}\n${indent}))` 37 | } else { 38 | // edge case: show the type of the property as the value 39 | displayValue = `?${property.type}?(\n${displayValue})` 40 | } 41 | } else { 42 | // for null, uninitialized, resource, etc. show the type 43 | displayValue = property.value || property.type === 'string' ? property.value : property.type 44 | if (property.type === 'string') { 45 | // escaping ? 46 | if (property.size > property.value.length) { 47 | // get value 48 | const p2 = await property.context.stackFrame.connection.sendPropertyValueNameCommand( 49 | property.fullName, 50 | property.context 51 | ) 52 | displayValue = p2.value 53 | } 54 | displayValue = `'${displayValue}'` 55 | } else if (property.type === 'bool') { 56 | displayValue = Boolean(parseInt(displayValue, 10)).toString() 57 | } 58 | } 59 | return displayValue 60 | } 61 | -------------------------------------------------------------------------------- /testproject/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | //"debugServer": 4711, // Uncomment for debugging the adapter 6 | "name": "Listen for Xdebug", 7 | "type": "php", 8 | "request": "launch", 9 | "port": 9003, 10 | "log": true 11 | }, 12 | { 13 | //"debugServer": 4711, // Uncomment for debugging the adapter 14 | "name": "Launch currently open script", 15 | "type": "php", 16 | "request": "launch", 17 | "program": "${file}", 18 | "cwd": "${fileDirname}", 19 | "port": 0, 20 | "runtimeArgs": ["-dxdebug.start_with_request=yes"], 21 | "env": { 22 | "XDEBUG_MODE": "debug,develop", 23 | "XDEBUG_CONFIG": "client_port=${port}" 24 | }, 25 | "ignoreExceptions": ["IgnoreException"] 26 | }, 27 | { 28 | //"debugServer": 4711, // Uncomment for debugging the adapter 29 | "name": "Launch Built-in web server", 30 | "type": "php", 31 | "request": "launch", 32 | "runtimeArgs": [ 33 | "-dxdebug.mode=debug", 34 | "-dxdebug.start_with_request=yes", 35 | "-dxdebug.client_port=${port}", 36 | "-S", 37 | "localhost:0" 38 | ], 39 | "program": "", 40 | "cwd": "${workspaceRoot}", 41 | "port": 0, 42 | "serverReadyAction": { 43 | "pattern": "Development Server \\(http://localhost:([0-9]+)\\) started", 44 | "uriFormat": "http://localhost:%s", 45 | "action": "openExternally" 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /testproject/envfile: -------------------------------------------------------------------------------- 1 | TEST1=VALUE1 2 | Test2=Value2 3 | -------------------------------------------------------------------------------- /testproject/error.php: -------------------------------------------------------------------------------- 1 | 'world'); 4 | 5 | // Notice 6 | trigger_error("Test notice", E_USER_NOTICE); 7 | 8 | // Warning 9 | trigger_error("Test warning", E_USER_WARNING); 10 | 11 | // Exception 12 | throw new Exception('this is an exception'); 13 | -------------------------------------------------------------------------------- /testproject/folder with spaces/file with spaces.php: -------------------------------------------------------------------------------- 1 | 2, 'test2' => ['t' => 123]); 6 | $aFloat = 1.23; 7 | $anInt = 123; 8 | $aString = '123'; 9 | $anEmptyString = ''; 10 | $aVeryLongString = str_repeat('lol', 1000); 11 | $aBoolean = true; 12 | $nullValue = null; 13 | $variableThatsNotSet; 14 | $aLargeArray = array_fill(0, 100, 'test'); 15 | $arrayWithSpaceKey = array('space key' => 1); 16 | $arrayExtended = array("a\0b" => "c\0d"); 17 | $arrayExtended2 = array("Приветствие" => "КУ-КУ", "Прощание" => "Па-Ка"); 18 | 19 | exit; 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "target": "es2019", 5 | "module": "commonjs", 6 | "rootDir": "src", 7 | "outDir": "out", 8 | "moduleResolution": "node", 9 | "noImplicitAny": true, 10 | "removeComments": false, 11 | "preserveConstEnums": true, 12 | "sourceMap": true, 13 | "strictNullChecks": true, 14 | "noImplicitReturns": true, 15 | "allowUnreachableCode": false, 16 | "noUnusedLocals": true, 17 | "esModuleInterop": true 18 | } 19 | } 20 | --------------------------------------------------------------------------------