├── .gitignore ├── .npmignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── bin └── vscode-bisect ├── build └── pipeline.yml ├── package-lock.json ├── package.json ├── src ├── bisect.ts ├── builds.ts ├── constants.ts ├── fetch.ts ├── files.ts ├── git.ts ├── index.ts ├── launcher.ts └── storage.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | .gitignore 3 | tsconfig.json 4 | **/*.js.map 5 | package-lock.json 6 | .vscode/ 7 | build/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Launch Program", 5 | "program": "${workspaceFolder}/bin/vscode-bisect", 6 | "request": "launch", 7 | "type": "node" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "out": true, 10 | ".vscode-test": true 11 | }, 12 | "search.exclude": { 13 | "**/node_modules": true, 14 | "**/bower_components": true, 15 | "**/*.code-search": true, 16 | "out": true 17 | }, 18 | "typescript.tsc.autoDetect": "off", 19 | "git.branchProtection": [ 20 | "main" 21 | ], 22 | "git.branchProtectionPrompt": "alwaysCommitToNewBranch", 23 | "git.branchRandomName.enable": true, 24 | "git.pullBeforeCheckout": true, 25 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 | # vscode-bisect 2 | Allows to bisect released VSCode web and desktop insider builds for issues similar to what `git bisect` does. 3 | 4 | [![Build Status](https://dev.azure.com/monacotools/Monaco/_apis/build/status%2Fnpm%2Fvscode%2Fvscode-bisect?repoName=microsoft%2Fvscode-bisect&branchName=main)](https://dev.azure.com/monacotools/Monaco/_build/latest?definitionId=505&repoName=microsoft%2Fvscode-bisect&branchName=main) 5 | 6 | ## Requirements 7 | 8 | - [Node.js](https://nodejs.org/en/) at least `16.x.x` 9 | 10 | ## Usage 11 | 12 | ```sh 13 | npx @vscode/vscode-bisect [ARGS] 14 | ``` 15 | 16 | ## Help 17 | 18 | ```sh 19 | npx @vscode/vscode-bisect --help 20 | ``` 21 | 22 | `@vscode/vscode-bisect` is meant to be only used as a command line tool. 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## How to file issues and get help 2 | 3 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 4 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 5 | feature request as a new Issue. 6 | 7 | ## Microsoft Support Policy 8 | 9 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 10 | -------------------------------------------------------------------------------- /bin/vscode-bisect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../out/index')(process.argv); -------------------------------------------------------------------------------- /build/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(Date:yyyyMMdd)$(Rev:.r) 2 | 3 | trigger: 4 | branches: 5 | include: 6 | - main 7 | pr: none 8 | 9 | resources: 10 | repositories: 11 | - repository: templates 12 | type: github 13 | name: microsoft/vscode-engineering 14 | ref: main 15 | endpoint: Monaco 16 | 17 | parameters: 18 | - name: publishPackage 19 | displayName: 🚀 Publish @vscode/vscode-bisect 20 | type: boolean 21 | default: false 22 | 23 | extends: 24 | template: azure-pipelines/npm-package/pipeline.yml@templates 25 | parameters: 26 | npmPackages: 27 | - name: vscode-bisect 28 | 29 | buildSteps: 30 | - script: npm ci 31 | displayName: Install dependencies 32 | 33 | - script: npm run build 34 | displayName: Build 35 | 36 | testPlatforms: [] 37 | 38 | publishPackage: ${{ parameters.publishPackage }} -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/vscode-bisect", 3 | "version": "0.5.6", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@vscode/vscode-bisect", 9 | "version": "0.5.6", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@vscode/vscode-perf": "^0.0.19", 13 | "chalk": "^4.x", 14 | "commander": "^9.4.0", 15 | "fflate": "^0.8.2", 16 | "follow-redirects": "^1.15.6", 17 | "open": "^8.4.0", 18 | "progress": "^2.0.3", 19 | "prompts": "^2.4.2", 20 | "simple-git": "^3.16.0", 21 | "tree-kill": "^1.2.2", 22 | "vscode-uri": "^3.0.8" 23 | }, 24 | "bin": { 25 | "vscode-bisect": "bin/vscode-bisect" 26 | }, 27 | "devDependencies": { 28 | "@types/follow-redirects": "^1.x", 29 | "@types/node": "20.x", 30 | "@types/progress": "^2.0.7", 31 | "@types/prompts": "^2.x", 32 | "typescript": "5.x" 33 | }, 34 | "engines": { 35 | "node": ">= 16" 36 | } 37 | }, 38 | "node_modules/@kwsites/file-exists": { 39 | "version": "1.1.1", 40 | "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", 41 | "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", 42 | "license": "MIT", 43 | "dependencies": { 44 | "debug": "^4.1.1" 45 | } 46 | }, 47 | "node_modules/@kwsites/promise-deferred": { 48 | "version": "1.1.1", 49 | "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", 50 | "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", 51 | "license": "MIT" 52 | }, 53 | "node_modules/@types/follow-redirects": { 54 | "version": "1.14.4", 55 | "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", 56 | "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", 57 | "dev": true, 58 | "license": "MIT", 59 | "dependencies": { 60 | "@types/node": "*" 61 | } 62 | }, 63 | "node_modules/@types/node": { 64 | "version": "20.12.11", 65 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", 66 | "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", 67 | "dev": true, 68 | "dependencies": { 69 | "undici-types": "~5.26.4" 70 | } 71 | }, 72 | "node_modules/@types/progress": { 73 | "version": "2.0.7", 74 | "resolved": "https://registry.npmjs.org/@types/progress/-/progress-2.0.7.tgz", 75 | "integrity": "sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w==", 76 | "dev": true, 77 | "license": "MIT", 78 | "dependencies": { 79 | "@types/node": "*" 80 | } 81 | }, 82 | "node_modules/@types/prompts": { 83 | "version": "2.0.14", 84 | "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.0.14.tgz", 85 | "integrity": "sha512-HZBd99fKxRWpYCErtm2/yxUZv6/PBI9J7N4TNFffl5JbrYMHBwF25DjQGTW3b3jmXq+9P6/8fCIb2ee57BFfYA==", 86 | "dev": true, 87 | "license": "MIT", 88 | "dependencies": { 89 | "@types/node": "*" 90 | } 91 | }, 92 | "node_modules/@vscode/vscode-perf": { 93 | "version": "0.0.19", 94 | "resolved": "https://registry.npmjs.org/@vscode/vscode-perf/-/vscode-perf-0.0.19.tgz", 95 | "integrity": "sha512-E/I0S+71K3Jo4kiMYbeKM8mUG3K8cHlj5MFVfPYVAvlp7KuIZTM914E7osp+jx8XgMLN6fChxnFmntm1GtVrKA==", 96 | "license": "MIT", 97 | "dependencies": { 98 | "chalk": "^4.x", 99 | "commander": "^9.4.0", 100 | "cookie": "^0.7.2", 101 | "js-base64": "^3.7.4", 102 | "node-fetch": "2.6.8", 103 | "playwright": "^1.29.2" 104 | }, 105 | "bin": { 106 | "vscode-perf": "bin/vscode-perf" 107 | }, 108 | "engines": { 109 | "node": ">= 16" 110 | } 111 | }, 112 | "node_modules/ansi-styles": { 113 | "version": "4.3.0", 114 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 115 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 116 | "license": "MIT", 117 | "dependencies": { 118 | "color-convert": "^2.0.1" 119 | }, 120 | "engines": { 121 | "node": ">=8" 122 | }, 123 | "funding": { 124 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 125 | } 126 | }, 127 | "node_modules/chalk": { 128 | "version": "4.1.2", 129 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 130 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 131 | "license": "MIT", 132 | "dependencies": { 133 | "ansi-styles": "^4.1.0", 134 | "supports-color": "^7.1.0" 135 | }, 136 | "engines": { 137 | "node": ">=10" 138 | }, 139 | "funding": { 140 | "url": "https://github.com/chalk/chalk?sponsor=1" 141 | } 142 | }, 143 | "node_modules/color-convert": { 144 | "version": "2.0.1", 145 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 146 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 147 | "license": "MIT", 148 | "dependencies": { 149 | "color-name": "~1.1.4" 150 | }, 151 | "engines": { 152 | "node": ">=7.0.0" 153 | } 154 | }, 155 | "node_modules/color-name": { 156 | "version": "1.1.4", 157 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 158 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 159 | "license": "MIT" 160 | }, 161 | "node_modules/commander": { 162 | "version": "9.4.0", 163 | "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", 164 | "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==", 165 | "license": "MIT", 166 | "engines": { 167 | "node": "^12.20.0 || >=14" 168 | } 169 | }, 170 | "node_modules/cookie": { 171 | "version": "0.7.2", 172 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 173 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 174 | "license": "MIT", 175 | "engines": { 176 | "node": ">= 0.6" 177 | } 178 | }, 179 | "node_modules/debug": { 180 | "version": "4.3.4", 181 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 182 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 183 | "license": "MIT", 184 | "dependencies": { 185 | "ms": "2.1.2" 186 | }, 187 | "engines": { 188 | "node": ">=6.0" 189 | }, 190 | "peerDependenciesMeta": { 191 | "supports-color": { 192 | "optional": true 193 | } 194 | } 195 | }, 196 | "node_modules/define-lazy-prop": { 197 | "version": "2.0.0", 198 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", 199 | "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", 200 | "license": "MIT", 201 | "engines": { 202 | "node": ">=8" 203 | } 204 | }, 205 | "node_modules/fflate": { 206 | "version": "0.8.2", 207 | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", 208 | "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" 209 | }, 210 | "node_modules/follow-redirects": { 211 | "version": "1.15.6", 212 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 213 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 214 | "funding": [ 215 | { 216 | "type": "individual", 217 | "url": "https://github.com/sponsors/RubenVerborgh" 218 | } 219 | ], 220 | "license": "MIT", 221 | "engines": { 222 | "node": ">=4.0" 223 | }, 224 | "peerDependenciesMeta": { 225 | "debug": { 226 | "optional": true 227 | } 228 | } 229 | }, 230 | "node_modules/fsevents": { 231 | "version": "2.3.2", 232 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 233 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 234 | "hasInstallScript": true, 235 | "optional": true, 236 | "os": [ 237 | "darwin" 238 | ], 239 | "engines": { 240 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 241 | } 242 | }, 243 | "node_modules/has-flag": { 244 | "version": "4.0.0", 245 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 246 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 247 | "license": "MIT", 248 | "engines": { 249 | "node": ">=8" 250 | } 251 | }, 252 | "node_modules/is-docker": { 253 | "version": "2.2.1", 254 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", 255 | "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", 256 | "license": "MIT", 257 | "bin": { 258 | "is-docker": "cli.js" 259 | }, 260 | "engines": { 261 | "node": ">=8" 262 | }, 263 | "funding": { 264 | "url": "https://github.com/sponsors/sindresorhus" 265 | } 266 | }, 267 | "node_modules/is-wsl": { 268 | "version": "2.2.0", 269 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", 270 | "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", 271 | "license": "MIT", 272 | "dependencies": { 273 | "is-docker": "^2.0.0" 274 | }, 275 | "engines": { 276 | "node": ">=8" 277 | } 278 | }, 279 | "node_modules/js-base64": { 280 | "version": "3.7.4", 281 | "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.4.tgz", 282 | "integrity": "sha512-wpM/wi20Tl+3ifTyi0RdDckS4YTD4Lf953mBRrpG8547T7hInHNPEj8+ck4gB8VDcGyeAWFK++Wb/fU1BeavKQ==", 283 | "license": "BSD-3-Clause" 284 | }, 285 | "node_modules/kleur": { 286 | "version": "3.0.3", 287 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", 288 | "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", 289 | "license": "MIT", 290 | "engines": { 291 | "node": ">=6" 292 | } 293 | }, 294 | "node_modules/ms": { 295 | "version": "2.1.2", 296 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 297 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 298 | "license": "MIT" 299 | }, 300 | "node_modules/node-fetch": { 301 | "version": "2.6.8", 302 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", 303 | "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", 304 | "license": "MIT", 305 | "dependencies": { 306 | "whatwg-url": "^5.0.0" 307 | }, 308 | "engines": { 309 | "node": "4.x || >=6.0.0" 310 | }, 311 | "peerDependencies": { 312 | "encoding": "^0.1.0" 313 | }, 314 | "peerDependenciesMeta": { 315 | "encoding": { 316 | "optional": true 317 | } 318 | } 319 | }, 320 | "node_modules/open": { 321 | "version": "8.4.0", 322 | "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", 323 | "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", 324 | "license": "MIT", 325 | "dependencies": { 326 | "define-lazy-prop": "^2.0.0", 327 | "is-docker": "^2.1.1", 328 | "is-wsl": "^2.2.0" 329 | }, 330 | "engines": { 331 | "node": ">=12" 332 | }, 333 | "funding": { 334 | "url": "https://github.com/sponsors/sindresorhus" 335 | } 336 | }, 337 | "node_modules/playwright": { 338 | "version": "1.44.0", 339 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz", 340 | "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==", 341 | "dependencies": { 342 | "playwright-core": "1.44.0" 343 | }, 344 | "bin": { 345 | "playwright": "cli.js" 346 | }, 347 | "engines": { 348 | "node": ">=16" 349 | }, 350 | "optionalDependencies": { 351 | "fsevents": "2.3.2" 352 | } 353 | }, 354 | "node_modules/playwright-core": { 355 | "version": "1.44.0", 356 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz", 357 | "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==", 358 | "bin": { 359 | "playwright-core": "cli.js" 360 | }, 361 | "engines": { 362 | "node": ">=16" 363 | } 364 | }, 365 | "node_modules/progress": { 366 | "version": "2.0.3", 367 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 368 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 369 | "license": "MIT", 370 | "engines": { 371 | "node": ">=0.4.0" 372 | } 373 | }, 374 | "node_modules/prompts": { 375 | "version": "2.4.2", 376 | "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", 377 | "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", 378 | "license": "MIT", 379 | "dependencies": { 380 | "kleur": "^3.0.3", 381 | "sisteransi": "^1.0.5" 382 | }, 383 | "engines": { 384 | "node": ">= 6" 385 | } 386 | }, 387 | "node_modules/simple-git": { 388 | "version": "3.16.0", 389 | "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.16.0.tgz", 390 | "integrity": "sha512-zuWYsOLEhbJRWVxpjdiXl6eyAyGo/KzVW+KFhhw9MqEEJttcq+32jTWSGyxTdf9e/YCohxRE+9xpWFj9FdiJNw==", 391 | "license": "MIT", 392 | "dependencies": { 393 | "@kwsites/file-exists": "^1.1.1", 394 | "@kwsites/promise-deferred": "^1.1.1", 395 | "debug": "^4.3.4" 396 | }, 397 | "funding": { 398 | "type": "github", 399 | "url": "https://github.com/steveukx/git-js?sponsor=1" 400 | } 401 | }, 402 | "node_modules/sisteransi": { 403 | "version": "1.0.5", 404 | "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", 405 | "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", 406 | "license": "MIT" 407 | }, 408 | "node_modules/supports-color": { 409 | "version": "7.2.0", 410 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 411 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 412 | "license": "MIT", 413 | "dependencies": { 414 | "has-flag": "^4.0.0" 415 | }, 416 | "engines": { 417 | "node": ">=8" 418 | } 419 | }, 420 | "node_modules/tr46": { 421 | "version": "0.0.3", 422 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 423 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 424 | "license": "MIT" 425 | }, 426 | "node_modules/tree-kill": { 427 | "version": "1.2.2", 428 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 429 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 430 | "license": "MIT", 431 | "bin": { 432 | "tree-kill": "cli.js" 433 | } 434 | }, 435 | "node_modules/typescript": { 436 | "version": "5.4.5", 437 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 438 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 439 | "dev": true, 440 | "bin": { 441 | "tsc": "bin/tsc", 442 | "tsserver": "bin/tsserver" 443 | }, 444 | "engines": { 445 | "node": ">=14.17" 446 | } 447 | }, 448 | "node_modules/undici-types": { 449 | "version": "5.26.5", 450 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 451 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 452 | "dev": true 453 | }, 454 | "node_modules/vscode-uri": { 455 | "version": "3.0.8", 456 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", 457 | "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" 458 | }, 459 | "node_modules/webidl-conversions": { 460 | "version": "3.0.1", 461 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 462 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 463 | "license": "BSD-2-Clause" 464 | }, 465 | "node_modules/whatwg-url": { 466 | "version": "5.0.0", 467 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 468 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 469 | "license": "MIT", 470 | "dependencies": { 471 | "tr46": "~0.0.3", 472 | "webidl-conversions": "^3.0.0" 473 | } 474 | } 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/vscode-bisect", 3 | "version": "0.5.6", 4 | "description": "Bisect released VS Code Insider builds to find bugs or performance issues similar to what git bisect supports.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/microsoft/vscode-bisect" 8 | }, 9 | "homepage": "https://github.com/microsoft/vscode-bisect", 10 | "keywords": [ 11 | "vscode" 12 | ], 13 | "bin": { 14 | "vscode-bisect": "bin/vscode-bisect" 15 | }, 16 | "license": "MIT", 17 | "authors": "microsoft", 18 | "main": "out/index", 19 | "scripts": { 20 | "compile": "tsc -p ./", 21 | "build": "tsc -p ./", 22 | "watch": "tsc -watch -p ./", 23 | "prepare": "npm run build" 24 | }, 25 | "engines": { 26 | "node": ">= 16" 27 | }, 28 | "dependencies": { 29 | "@vscode/vscode-perf": "^0.0.19", 30 | "chalk": "^4.x", 31 | "commander": "^9.4.0", 32 | "fflate": "^0.8.2", 33 | "follow-redirects": "^1.15.6", 34 | "open": "^8.4.0", 35 | "progress": "^2.0.3", 36 | "prompts": "^2.4.2", 37 | "simple-git": "^3.16.0", 38 | "tree-kill": "^1.2.2", 39 | "vscode-uri": "^3.0.8" 40 | }, 41 | "devDependencies": { 42 | "@types/follow-redirects": "^1.x", 43 | "@types/node": "20.x", 44 | "@types/progress": "^2.0.7", 45 | "@types/prompts": "^2.x", 46 | "typescript": "5.x" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/bisect.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import prompts from 'prompts'; 7 | import chalk from 'chalk'; 8 | import open from 'open'; 9 | import { builds, IBuild } from './builds'; 10 | import { logTroubleshoot, Runtime } from './constants'; 11 | import { launcher } from './launcher'; 12 | 13 | enum BisectResponse { 14 | Good = 1, 15 | Bad, 16 | Quit 17 | } 18 | 19 | interface IBisectState { 20 | currentChunk: number; 21 | currentIndex: number; 22 | } 23 | 24 | class Bisecter { 25 | 26 | async start(runtime: Runtime = Runtime.WebLocal, goodCommitOrVersion?: string, badCommitOrVersion?: string, releasedOnly?: boolean): Promise { 27 | 28 | // Resolve commits from input 29 | const { goodCommit, badCommit } = await this.resolveCommits(runtime, goodCommitOrVersion, badCommitOrVersion); 30 | 31 | // Get builds to bisect 32 | const buildsRange = await builds.fetchBuilds(runtime, goodCommit, badCommit, releasedOnly); 33 | 34 | console.log(`${chalk.gray('[build]')} total ${chalk.green(buildsRange.length)} builds with roughly ${chalk.green(Math.round(Math.log2(buildsRange.length)))} steps`); 35 | 36 | let goodBuild: IBuild | undefined = undefined; 37 | let badBuild: IBuild | undefined = undefined; 38 | let build: IBuild; 39 | let quit = false; 40 | 41 | if (buildsRange.length < 2) { 42 | return this.finishBisect(badBuild, goodBuild); 43 | } 44 | 45 | // Start bisecting via binary search 46 | 47 | const state = { currentChunk: buildsRange.length, currentIndex: 0 }; 48 | this.nextState(state, BisectResponse.Bad /* try older */); 49 | 50 | // Go over next builds for as long as we are not done... 51 | while (build = buildsRange[state.currentIndex]) { 52 | const response = await this.tryBuild(build, { isBisecting: true, forceReDownload: false }); 53 | if (response === BisectResponse.Bad) { 54 | badBuild = build; 55 | } else if (response === BisectResponse.Good) { 56 | goodBuild = build; 57 | } else { 58 | quit = true; 59 | break; 60 | } 61 | 62 | const finished = this.nextState(state, response); 63 | if (finished) { 64 | break; 65 | } 66 | } 67 | 68 | if (!quit) { 69 | return this.finishBisect(badBuild, goodBuild); 70 | } 71 | } 72 | 73 | private async resolveCommits(runtime: Runtime, goodCommitOrVersion?: string, badCommitOrVersion?: string) { 74 | return { 75 | goodCommit: await this.resolveCommit(runtime, goodCommitOrVersion), 76 | badCommit: await this.resolveCommit(runtime, badCommitOrVersion) 77 | }; 78 | } 79 | 80 | private async resolveCommit(runtime: Runtime, commitOrVersion?: string) { 81 | if (!commitOrVersion) { 82 | return undefined; 83 | } 84 | 85 | if (/^\d+\.\d+$/.test(commitOrVersion)) { 86 | const commit = (await builds.fetchBuildByVersion(runtime, commitOrVersion)).commit; 87 | console.log(`${chalk.gray('[build]')} latest insiders build with version ${chalk.green(commitOrVersion)} is ${chalk.green(commit)}.`); 88 | return commit; 89 | } 90 | 91 | if (/^[0-9a-f]{40}$/i.test(commitOrVersion)) { 92 | return commitOrVersion; 93 | } 94 | 95 | throw new Error(`Invalid commit or version format. Please provide a valid Git commit hash or version in the format of ${chalk.green('major.minor')}.`); 96 | } 97 | 98 | private async finishBisect(badBuild: IBuild | undefined, goodBuild: IBuild | undefined): Promise { 99 | if (goodBuild && badBuild) { 100 | console.log(`${chalk.gray('[build]')} ${chalk.green(badBuild.commit)} is the first bad commit after ${chalk.green(goodBuild.commit)}.`); 101 | 102 | const response = await prompts([ 103 | { 104 | type: 'confirm', 105 | name: 'open', 106 | initial: true, 107 | message: 'Would you like to open GitHub for the list of changes?', 108 | 109 | } 110 | ]); 111 | 112 | if (response.open) { 113 | open(`https://github.com/microsoft/vscode/compare/${goodBuild.commit}...${badBuild.commit}`); 114 | } 115 | 116 | console.log(` 117 | Run the following commands to continue bisecting via git in a folder where VS Code is checked out to: 118 | 119 | ${chalk.green(`git bisect start && git bisect bad ${badBuild.commit} && git bisect good ${goodBuild.commit}`)} 120 | 121 | `); 122 | } else if (badBuild) { 123 | console.log(`${chalk.gray('[build]')} ${chalk.red('All builds are bad!')} Try running with ${chalk.green('--releasedOnly')} to support older builds.`); 124 | } else if (goodBuild) { 125 | console.log(`${chalk.gray('[build]')} ${chalk.green('All builds are good!')} Try running with ${chalk.green('--releasedOnly')} to support older builds.`); 126 | } else { 127 | console.log(`${chalk.gray('[build]')} ${chalk.red('No builds bisected. Bisect needs at least 2 builds from "main" branch to work.')}`); 128 | } 129 | } 130 | 131 | private nextState(state: IBisectState, response: BisectResponse): boolean { 132 | 133 | // Binary search is done 134 | if (state.currentChunk === 1) { 135 | return true; 136 | } 137 | 138 | // Binary search is not done 139 | else { 140 | state.currentChunk = Math.round(state.currentChunk / 2); 141 | state.currentIndex = response === BisectResponse.Good ? state.currentIndex - state.currentChunk /* try newer */ : state.currentIndex + state.currentChunk /* try older */; 142 | 143 | return false; 144 | } 145 | } 146 | 147 | async tryBuild(build: IBuild, options: { forceReDownload: boolean, isBisecting: boolean }): Promise { 148 | try { 149 | const instance = await launcher.launch(build, options); 150 | 151 | const response = options.isBisecting ? await prompts([ 152 | { 153 | type: 'select', 154 | name: 'status', 155 | message: `Is ${chalk.green(build.commit)} good or bad?`, 156 | choices: [ 157 | { title: 'Good', value: 'good' }, 158 | { title: 'Bad', value: 'bad' }, 159 | { title: 'Retry', value: 'retry' } 160 | ] 161 | } 162 | ]) : await prompts([ 163 | { 164 | type: 'select', 165 | name: 'status', 166 | message: `Would you like to restart ${chalk.green(build.commit)}?`, 167 | choices: [ 168 | { title: 'Yes', value: 'retry' }, 169 | { title: 'No', value: 'no' } 170 | ] 171 | } 172 | ]); 173 | 174 | await instance.stop(); 175 | 176 | if (response.status === 'retry') { 177 | return this.tryBuild(build, { forceReDownload: false, isBisecting: options.isBisecting }); 178 | } 179 | 180 | return response.status === 'good' ? BisectResponse.Good : response.status === 'bad' ? BisectResponse.Bad : BisectResponse.Quit; 181 | } catch (error) { 182 | console.log(`${chalk.red('\n[error]')} ${error}\n`); 183 | 184 | const response = await prompts([ 185 | { 186 | type: 'select', 187 | name: 'status', 188 | message: `Would you like to retry?`, 189 | choices: [ 190 | { title: 'Yes', value: 'yes' }, 191 | { title: 'No', value: 'no' } 192 | ] 193 | } 194 | ]); 195 | 196 | if (response.status === 'yes') { 197 | return this.tryBuild(build, { forceReDownload: true, isBisecting: options.isBisecting }); 198 | } 199 | 200 | logTroubleshoot(); 201 | 202 | return BisectResponse.Quit; 203 | } 204 | } 205 | } 206 | 207 | export const bisecter = new Bisecter(); -------------------------------------------------------------------------------- /src/builds.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import chalk from 'chalk'; 7 | import { dirname, join } from 'path'; 8 | import { rmSync } from 'fs'; 9 | import { LOGGER, Platform, platform, Runtime } from './constants'; 10 | import { fileGet, jsonGet } from './fetch'; 11 | import { computeSHA256, exists, getBuildPath, unzip } from './files'; 12 | 13 | export interface IBuild { 14 | readonly runtime: Runtime; 15 | readonly commit: string; 16 | } 17 | 18 | interface IBuildMetadata { 19 | readonly url: string; 20 | readonly version: string; 21 | readonly productVersion: string; 22 | readonly sha256hash: string; 23 | } 24 | 25 | class Builds { 26 | 27 | async fetchBuildByVersion(runtime = Runtime.WebLocal, version: string): Promise { 28 | const meta = await jsonGet(`https://update.code.visualstudio.com/api/versions/${version}.0-insider/${this.getBuildApiName(runtime)}/insider?released=true`); 29 | 30 | return { runtime, commit: meta.version }; 31 | } 32 | 33 | async fetchBuilds(runtime = Runtime.WebLocal, goodCommit?: string, badCommit?: string, releasedOnly?: boolean): Promise { 34 | 35 | // Fetch all released insider builds 36 | const allBuilds = await this.fetchAllBuilds(runtime, releasedOnly); 37 | 38 | let goodCommitIndex = allBuilds.length - 1; // last build (oldest) by default 39 | let badCommitIndex = 0; // first build (newest) by default 40 | 41 | if (typeof goodCommit === 'string') { 42 | const candidateGoodCommitIndex = this.indexOf(goodCommit, allBuilds); 43 | if (typeof candidateGoodCommitIndex !== 'number') { 44 | if (releasedOnly) { 45 | throw new Error(`Provided good commit ${chalk.green(goodCommit)} was not found in the list of insiders builds. It is either invalid or too old.`); 46 | } else { 47 | return this.fetchBuilds(runtime, goodCommit, badCommit, true); 48 | } 49 | } 50 | 51 | goodCommitIndex = candidateGoodCommitIndex; 52 | } 53 | 54 | if (typeof badCommit === 'string') { 55 | const candidateBadCommitIndex = this.indexOf(badCommit, allBuilds); 56 | if (typeof candidateBadCommitIndex !== 'number') { 57 | if (releasedOnly) { 58 | throw new Error(`Provided bad commit ${chalk.green(badCommit)} was not found in the list of insiders builds. It is either invalid or too old.`); 59 | } else { 60 | return this.fetchBuilds(runtime, goodCommit, badCommit, true); 61 | } 62 | } 63 | 64 | badCommitIndex = candidateBadCommitIndex; 65 | } 66 | 67 | if (badCommitIndex >= goodCommitIndex) { 68 | throw new Error(`Provided bad commit ${chalk.green(badCommit)} cannot be older or same as good commit ${chalk.green(goodCommit)}.`); 69 | } 70 | 71 | // Build a range based on the bad and good commits if any 72 | const buildsInRange = allBuilds.slice(badCommitIndex, goodCommitIndex + 1); 73 | 74 | // Drop those builds that are not on main branch 75 | return buildsInRange; 76 | } 77 | 78 | private indexOf(commit: string, builds: IBuild[]): number | undefined { 79 | for (let i = 0; i < builds.length; i++) { 80 | const build = builds[i]; 81 | if (build.commit === commit) { 82 | return i; 83 | } 84 | } 85 | 86 | return undefined; 87 | } 88 | 89 | private async fetchAllBuilds(runtime: Runtime, releasedOnly = false): Promise { 90 | const url = `https://update.code.visualstudio.com/api/commits/insider/${this.getBuildApiName(runtime)}?released=${releasedOnly}`; 91 | console.log(`${chalk.gray('[build]')} fetching all builds from ${chalk.green(url)}...`); 92 | const commits = await jsonGet>(url); 93 | 94 | return commits.map(commit => ({ commit, runtime })); 95 | } 96 | 97 | private getBuildApiName(runtime: Runtime): string { 98 | switch (runtime) { 99 | case Runtime.WebLocal: 100 | case Runtime.WebRemote: 101 | switch (platform) { 102 | case Platform.MacOSX64: 103 | case Platform.MacOSArm: 104 | return 'server-darwin-web'; 105 | case Platform.LinuxX64: 106 | case Platform.LinuxArm: 107 | return 'server-linux-x64-web'; 108 | case Platform.WindowsX64: 109 | case Platform.WindowsArm: 110 | return 'server-win32-x64-web'; 111 | } 112 | 113 | case Runtime.DesktopLocal: 114 | switch (platform) { 115 | case Platform.MacOSX64: 116 | return 'darwin'; 117 | case Platform.MacOSArm: 118 | return 'darwin-arm64'; 119 | case Platform.LinuxX64: 120 | return 'linux-x64'; 121 | case Platform.LinuxArm: 122 | return 'linux-arm64'; 123 | case Platform.WindowsX64: 124 | return 'win32-x64'; 125 | case Platform.WindowsArm: 126 | return 'win32-arm64'; 127 | } 128 | } 129 | } 130 | 131 | async installBuild({ runtime, commit }: IBuild, options?: { forceReDownload: boolean }): Promise { 132 | const buildName = await this.getBuildArchiveName({ runtime, commit }); 133 | 134 | const path = join(getBuildPath(commit), buildName); 135 | 136 | const pathExists = await exists(path); 137 | if (pathExists && !options?.forceReDownload) { 138 | if (LOGGER.verbose) { 139 | console.log(`${chalk.gray('[build]')} using ${chalk.green(path)} for the next build to try`); 140 | } 141 | 142 | return; // assume the build is cached 143 | } 144 | 145 | if (pathExists && options?.forceReDownload) { 146 | console.log(`${chalk.gray('[build]')} deleting ${chalk.green(getBuildPath(commit))} and retrying download`); 147 | rmSync(getBuildPath(commit), { recursive: true }); 148 | } 149 | 150 | // Download 151 | const { url, sha256hash: expectedSHA256 } = await this.fetchBuildMeta({ runtime, commit }); 152 | console.log(`${chalk.gray('[build]')} downloading build from ${chalk.green(url)}...`); 153 | await fileGet(url, path); 154 | 155 | // Validate SHA256 Checksum 156 | const computedSHA256 = await computeSHA256(path); 157 | if (expectedSHA256 !== computedSHA256) { 158 | throw new Error(`${chalk.gray('[build]')} ${chalk.red('✘')} expected SHA256 checksum (${expectedSHA256}) does not match with download (${computedSHA256})`); 159 | } else { 160 | console.log(`${chalk.gray('[build]')} ${chalk.green('✔︎')} expected SHA256 checksum matches with download`); 161 | } 162 | 163 | // Unzip 164 | let destination: string; 165 | if (runtime === Runtime.DesktopLocal && platform === Platform.WindowsX64 || platform === Platform.WindowsArm) { 166 | // zip does not contain a single top level folder to use... 167 | destination = path.substring(0, path.lastIndexOf('.zip')); 168 | } else { 169 | // zip contains a single top level folder to use 170 | destination = dirname(path); 171 | } 172 | console.log(`${chalk.gray('[build]')} unzipping build to ${chalk.green(destination)}...`); 173 | await unzip(path, destination); 174 | } 175 | 176 | private async getBuildArchiveName({ runtime, commit }: IBuild): Promise { 177 | switch (runtime) { 178 | 179 | // We currently do not have ARM enabled servers 180 | // so we fallback to x64 until we ship ARM. 181 | case Runtime.WebLocal: 182 | case Runtime.WebRemote: 183 | switch (platform) { 184 | case Platform.MacOSX64: 185 | return 'vscode-server-darwin-x64-web.zip'; 186 | case Platform.MacOSArm: 187 | return 'vscode-server-darwin-arm64-web.zip'; 188 | case Platform.LinuxX64: 189 | return 'vscode-server-linux-arm64-web.tar.gz'; 190 | case Platform.LinuxArm: 191 | return 'vscode-server-linux-x64-web.tar.gz'; 192 | case Platform.WindowsX64: 193 | case Platform.WindowsArm: 194 | return 'vscode-server-win32-x64-web.zip'; 195 | } 196 | 197 | // Every platform has its own name scheme, hilarious right? 198 | // - macOS: just the name, nice! (e.g. VSCode-darwin.zip) 199 | // - Linux: includes some unix timestamp (e.g. code-insider-x64-1639979337.tar.gz) 200 | // - Windows: includes the version (e.g. VSCode-win32-x64-1.64.0-insider.zip) 201 | case Runtime.DesktopLocal: 202 | switch (platform) { 203 | case Platform.MacOSX64: 204 | return 'VSCode-darwin.zip'; 205 | case Platform.MacOSArm: 206 | return 'VSCode-darwin-arm64.zip'; 207 | case Platform.LinuxX64: 208 | case Platform.LinuxArm: 209 | return (await this.fetchBuildMeta({ runtime, commit })).url.split('/').pop()!; // e.g. https://az764295.vo.msecnd.net/insider/807bf598bea406dcb272a9fced54697986e87768/code-insider-x64-1639979337.tar.gz 210 | case Platform.WindowsX64: 211 | case Platform.WindowsArm: { 212 | const buildMeta = await this.fetchBuildMeta({ runtime, commit }); 213 | 214 | return platform === Platform.WindowsX64 ? `VSCode-win32-x64-${buildMeta.productVersion}.zip` : `VSCode-win32-arm64-${buildMeta.productVersion}.zip`; 215 | } 216 | } 217 | } 218 | } 219 | 220 | async getBuildName({ runtime, commit }: IBuild): Promise { 221 | switch (runtime) { 222 | case Runtime.WebLocal: 223 | case Runtime.WebRemote: 224 | switch (platform) { 225 | case Platform.MacOSX64: 226 | return 'vscode-server-darwin-x64-web'; 227 | case Platform.MacOSArm: 228 | return 'vscode-server-darwin-arm64-web'; 229 | case Platform.LinuxX64: 230 | return 'vscode-server-linux-arm64-web'; 231 | case Platform.LinuxArm: 232 | return 'vscode-server-linux-x64-web'; 233 | case Platform.WindowsX64: 234 | case Platform.WindowsArm: 235 | return 'vscode-server-win32-x64-web'; 236 | } 237 | 238 | // Here, only Windows does not play by our rules and adds the version number 239 | // - Windows: includes the version (e.g. VSCode-win32-x64-1.64.0-insider) 240 | case Runtime.DesktopLocal: 241 | switch (platform) { 242 | case Platform.MacOSX64: 243 | case Platform.MacOSArm: 244 | return 'Visual Studio Code - Insiders.app'; 245 | case Platform.LinuxX64: 246 | return 'VSCode-linux-x64'; 247 | case Platform.LinuxArm: 248 | return 'VSCode-linux-arm64'; 249 | case Platform.WindowsX64: 250 | case Platform.WindowsArm: { 251 | const buildMeta = await this.fetchBuildMeta({ runtime, commit }); 252 | 253 | return platform === Platform.WindowsX64 ? `VSCode-win32-x64-${buildMeta.productVersion}` : `VSCode-win32-arm64-${buildMeta.productVersion}`; 254 | } 255 | } 256 | } 257 | } 258 | 259 | private getPlatformName(runtime: Runtime): string { 260 | switch (runtime) { 261 | case Runtime.WebLocal: 262 | case Runtime.WebRemote: 263 | switch (platform) { 264 | case Platform.MacOSX64: 265 | return 'server-darwin-web'; 266 | case Platform.MacOSArm: 267 | return 'server-darwin-arm64-web'; 268 | case Platform.LinuxX64: 269 | return 'server-linux-x64-web'; 270 | case Platform.LinuxArm: 271 | return 'server-linux-arm64-web'; 272 | case Platform.WindowsX64: 273 | case Platform.WindowsArm: 274 | return 'server-win32-x64-web'; 275 | } 276 | 277 | case Runtime.DesktopLocal: 278 | switch (platform) { 279 | case Platform.MacOSX64: 280 | return 'darwin'; 281 | case Platform.MacOSArm: 282 | return 'darwin-arm64'; 283 | case Platform.LinuxX64: 284 | return 'linux-x64'; 285 | case Platform.LinuxArm: 286 | return 'linux-arm64'; 287 | case Platform.WindowsX64: 288 | return 'win32-x64-archive'; 289 | case Platform.WindowsArm: { 290 | return 'win32-arm64-archive'; 291 | } 292 | } 293 | } 294 | } 295 | 296 | private fetchBuildMeta({ runtime, commit }: IBuild): Promise { 297 | return jsonGet(`https://update.code.visualstudio.com/api/versions/commit:${commit}/${this.getPlatformName(runtime)}/insider`); 298 | } 299 | 300 | async getBuildExecutable({ runtime, commit }: IBuild): Promise { 301 | const buildPath = getBuildPath(commit); 302 | const buildName = await builds.getBuildName({ runtime, commit }); 303 | 304 | switch (runtime) { 305 | case Runtime.WebLocal: 306 | case Runtime.WebRemote: 307 | switch (platform) { 308 | case Platform.MacOSX64: 309 | case Platform.MacOSArm: 310 | case Platform.LinuxX64: 311 | case Platform.LinuxArm: { 312 | const oldLocation = join(buildPath, buildName, 'server.sh'); 313 | if (await exists(oldLocation)) { 314 | return oldLocation; // only valid until 1.64.x 315 | } 316 | 317 | return join(buildPath, buildName, 'bin', 'code-server-insiders'); 318 | } 319 | case Platform.WindowsX64: 320 | case Platform.WindowsArm: { 321 | const oldLocation = join(buildPath, buildName, 'server.cmd'); 322 | if (await exists(oldLocation)) { 323 | return oldLocation; // only valid until 1.64.x 324 | } 325 | 326 | return join(buildPath, buildName, 'bin', 'code-server-insiders.cmd'); 327 | } 328 | } 329 | 330 | case Runtime.DesktopLocal: 331 | switch (platform) { 332 | case Platform.MacOSX64: 333 | case Platform.MacOSArm: 334 | return join(buildPath, buildName, 'Contents', 'MacOS', 'Electron') 335 | case Platform.LinuxX64: 336 | case Platform.LinuxArm: 337 | return join(buildPath, buildName, 'code-insiders') 338 | case Platform.WindowsX64: 339 | case Platform.WindowsArm: 340 | return join(buildPath, buildName, 'Code - Insiders.exe') 341 | } 342 | } 343 | } 344 | } 345 | 346 | export const builds = new Builds(); 347 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import chalk from 'chalk'; 7 | import { tmpdir } from 'os'; 8 | import { join } from 'path'; 9 | 10 | export const ROOT = join(tmpdir(), 'vscode-bisect'); 11 | 12 | export const BUILD_FOLDER = join(ROOT, '.builds'); 13 | 14 | export const DATA_FOLDER = join(ROOT, '.data'); 15 | export const USER_DATA_FOLDER = join(DATA_FOLDER, 'data'); 16 | export const EXTENSIONS_FOLDER = join(DATA_FOLDER, 'extensions'); 17 | 18 | export const GIT_FOLDER = join(ROOT, 'git'); 19 | export const GIT_VSCODE_FOLDER = join(GIT_FOLDER, 'vscode'); 20 | export const GIT_REPO = 'https://github.com/microsoft/vscode.git'; 21 | 22 | export const STORAGE_FILE = join(ROOT, 'storage.json'); 23 | 24 | export const DEFAULT_PERFORMANCE_FILE = join(ROOT, 'startup-perf.txt'); 25 | export const PERFORMANCE_RUNS = 10; 26 | export const PERFORMANCE_RUN_TIMEOUT = 60000; 27 | 28 | export const VSCODE_DEV_URL = function (commit: string) { 29 | if (CONFIG.token) { 30 | return `https://insiders.vscode.dev/github/microsoft/vscode/blob/main/package.json?vscode-version=${commit}`; // with auth state, we can use `github` route 31 | } 32 | 33 | return `https://insiders.vscode.dev/?vscode-version=${commit}`; 34 | } 35 | 36 | export enum Platform { 37 | MacOSX64 = 1, 38 | MacOSArm, 39 | LinuxX64, 40 | LinuxArm, 41 | WindowsX64, 42 | WindowsArm 43 | } 44 | 45 | export const platform = (() => { 46 | if (process.platform === 'win32') { 47 | return process.arch === 'arm64' ? Platform.WindowsArm : Platform.WindowsX64; 48 | } 49 | 50 | if (process.platform === 'darwin') { 51 | return process.arch === 'arm64' ? Platform.MacOSArm : Platform.MacOSX64; 52 | } 53 | 54 | if (process.platform === 'linux') { 55 | return process.arch === 'arm64' ? Platform.LinuxArm : Platform.LinuxX64; 56 | } 57 | 58 | throw new Error('Unsupported platform.'); 59 | })(); 60 | 61 | export enum Runtime { 62 | WebLocal = 1, 63 | WebRemote, 64 | DesktopLocal 65 | } 66 | 67 | export const LOGGER = { 68 | verbose: false 69 | } 70 | 71 | export const CONFIG = { 72 | performance: false as boolean | string, 73 | token: undefined as string | undefined, 74 | } 75 | 76 | export function logTroubleshoot(): void { 77 | const packageJson = require('../package.json'); 78 | 79 | console.log(`\n${chalk.bold('Error Troubleshooting Guide:')} 80 | - run ${chalk.green('vscode-bisect --verbose')} for more detailed output 81 | - run ${chalk.green('vscode-bisect --reset')} to delete the cache folder 82 | - run ${chalk.green(`npm install -g ${packageJson.name}`)} to update to the latest version (your version: ${chalk.green(packageJson.version)})`); 83 | } -------------------------------------------------------------------------------- /src/fetch.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { https } from 'follow-redirects'; 7 | import { createWriteStream, promises } from 'fs'; 8 | import { dirname } from 'path'; 9 | import { OutgoingHttpHeaders } from 'http'; 10 | import chalk from 'chalk'; 11 | import ProgressBar from 'progress'; 12 | 13 | export function jsonGet(url: string, headers?: OutgoingHttpHeaders): Promise { 14 | return new Promise((resolve, reject) => { 15 | https.get(url, { headers }, res => { 16 | if (res.statusCode !== 200) { 17 | reject(`Failed to get response from update server (code: ${res.statusCode}, message: ${res.statusMessage})`); 18 | return; 19 | } 20 | 21 | let data = ''; 22 | 23 | res.on('data', chunk => data += chunk); 24 | res.on('end', () => resolve(JSON.parse(data))); 25 | res.on('error', err => reject(err)); 26 | }); 27 | }); 28 | } 29 | 30 | export async function fileGet(url: string, path: string): Promise { 31 | 32 | // Ensure parent folder exists 33 | await promises.mkdir(dirname(path), { recursive: true }); 34 | 35 | // Download 36 | return new Promise((resolve, reject) => { 37 | const request = https.get(url, res => { 38 | if (res.statusCode !== 200) { 39 | reject(`Failed to download file from update server (code: ${res.statusCode}, message: ${res.statusMessage})`); 40 | return; 41 | } 42 | 43 | const totalSize = parseInt(res.headers['content-length']!, 10); 44 | 45 | const bar = new ProgressBar(`${chalk.gray('[fetch]')} [:bar] :percent of ${(totalSize / 1024 / 1024).toFixed(2)} MB (:rate MB/s)`, { 46 | complete: '=', 47 | incomplete: ' ', 48 | width: 30, 49 | total: totalSize / (1024 * 1024), 50 | clear: true 51 | }); 52 | 53 | const outStream = createWriteStream(path); 54 | outStream.on('close', () => resolve()); 55 | outStream.on('error', reject); 56 | 57 | res.on('data', chunk => { 58 | bar.tick(chunk.length / (1024 * 1024)); 59 | }); 60 | 61 | res.on('error', reject); 62 | res.pipe(outStream); 63 | }); 64 | 65 | request.on('error', reject); 66 | }); 67 | } -------------------------------------------------------------------------------- /src/files.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { spawnSync } from 'child_process'; 7 | import { mkdirSync, promises, readFileSync, writeFileSync } from 'fs'; 8 | import { join } from 'path'; 9 | import { createHash } from 'crypto'; 10 | import { BUILD_FOLDER, Platform, platform } from './constants'; 11 | import { unzipSync } from 'fflate'; 12 | 13 | export async function exists(path: string): Promise { 14 | try { 15 | await promises.stat(path); 16 | 17 | return true; 18 | } catch (error) { 19 | return false; 20 | } 21 | } 22 | 23 | export function getBuildPath(commit: string): string { 24 | if (platform === Platform.WindowsX64 || platform === Platform.WindowsArm) { 25 | return join(BUILD_FOLDER, commit.substring(0, 6)); // keep the folder path small for windows max path length restrictions 26 | } 27 | 28 | return join(BUILD_FOLDER, commit); 29 | } 30 | 31 | export async function unzip(source: string, destination: string): Promise { 32 | 33 | // *.zip: macOS, Windows 34 | if (source.endsWith('.zip')) { 35 | 36 | // Windows 37 | if (platform === Platform.WindowsX64 || platform === Platform.WindowsArm) { 38 | const unzipped = unzipSync(readFileSync(source)); 39 | for (const entry of Object.keys(unzipped)) { 40 | if (entry.endsWith('/')) { 41 | mkdirSync(join(destination, entry), { recursive: true }); 42 | } else { 43 | writeFileSync(join(destination, entry), unzipped[entry]); 44 | } 45 | } 46 | } 47 | 48 | // macOS 49 | else { 50 | spawnSync('unzip', [source, '-d', destination]); 51 | } 52 | } 53 | 54 | // *.tar.gz: Linux 55 | else { 56 | if (!await exists(destination)) { 57 | await promises.mkdir(destination); // tar does not create extractDir by default 58 | } 59 | 60 | spawnSync('tar', ['-xzf', source, '-C', destination]); 61 | } 62 | } 63 | 64 | export async function computeSHA256(path: string): Promise { 65 | const fileBuffer = await promises.readFile(path); 66 | 67 | return createHash('sha256').update(fileBuffer).digest('hex'); 68 | } 69 | -------------------------------------------------------------------------------- /src/git.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import chalk from 'chalk'; 7 | import { mkdirSync } from 'fs'; 8 | import simpleGit from 'simple-git'; 9 | import { GIT_FOLDER, GIT_REPO, GIT_VSCODE_FOLDER } from './constants'; 10 | import { exists } from './files'; 11 | 12 | class Git { 13 | 14 | static { 15 | mkdirSync(GIT_FOLDER, { recursive: true }); 16 | } 17 | 18 | private _whenReady: Promise | undefined = undefined; 19 | get whenReady(): Promise { 20 | if (!this._whenReady) { 21 | this._whenReady = this.init(); 22 | } 23 | 24 | return this._whenReady; 25 | } 26 | 27 | private async init(): Promise { 28 | 29 | // Bring up to date otherwise 30 | if (await exists(GIT_VSCODE_FOLDER)) { 31 | console.log(`${chalk.gray('[git]')} pulling latest changes into ${chalk.green(GIT_FOLDER)}...`); 32 | 33 | const git = simpleGit({ baseDir: GIT_VSCODE_FOLDER }); 34 | await git.checkout('main'); 35 | await git.pull(); 36 | } 37 | 38 | // Clone repo if it does not exist 39 | else { 40 | console.log(`${chalk.gray('[git]')} cloning VS Code into ${chalk.green(GIT_VSCODE_FOLDER)} (this is only done once and can take a while)...`); 41 | 42 | const git = simpleGit(); 43 | await git.clone(GIT_REPO, GIT_VSCODE_FOLDER); 44 | } 45 | } 46 | } 47 | 48 | export const git = new Git(); -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import chalk from 'chalk'; 7 | import { program, Option } from 'commander'; 8 | import { rmSync, truncateSync } from 'fs'; 9 | import prompts from 'prompts'; 10 | import { bisecter } from './bisect'; 11 | import { git } from './git'; 12 | import { BUILD_FOLDER, CONFIG, LOGGER, logTroubleshoot, ROOT, Runtime } from './constants'; 13 | import { launcher } from './launcher'; 14 | import { builds } from './builds'; 15 | import { resolve } from 'path'; 16 | import { exists } from './files'; 17 | 18 | module.exports = async function (argv: string[]): Promise { 19 | 20 | interface Opts { 21 | runtime?: 'web' | 'desktop' | 'vscode.dev'; 22 | good?: string; 23 | bad?: string; 24 | commit?: string; 25 | version?: string; 26 | releasedOnly?: boolean; 27 | verbose?: boolean; 28 | reset?: boolean; 29 | perf?: boolean | string; 30 | token?: string; 31 | } 32 | 33 | program.addHelpText('beforeAll', `Version: ${require('../package.json').version}\n`); 34 | 35 | program 36 | .addOption(new Option('-r, --runtime ', 'whether to bisect with a local web, online vscode.dev or local desktop (default) version').choices(['desktop', 'web', 'vscode.dev'])) 37 | .option('-g, --good ', 'commit hash or version of a published insiders build that does not reproduce the issue') 38 | .option('-b, --bad ', 'commit hash or version of a published insiders build that reproduces the issue') 39 | .option('-c, --commit ', 'commit hash of a published insiders build to test or "latest" released build (supercedes -g and -b)') 40 | .option('-v, --version ', 'version of a published insiders build to test, for example 1.93 (supercedes -g, -b and -c)') 41 | .option('--releasedOnly', 'only bisect over released insiders builds to support older builds') 42 | .option('--reset', 'deletes the cache folder (use only for troubleshooting)') 43 | .addOption(new Option('-p, --perf [path]', 'runs a performance test and optionally writes the result to the provided path').hideHelp()) 44 | .addOption(new Option('-t, --token ', `a GitHub token of scopes 'repo', 'workflow', 'user:email', 'read:user' to enable additional performance tests targetting web`).hideHelp()) 45 | .option('--verbose', 'logs verbose output to the console when errors occur'); 46 | 47 | program.addHelpText('after', ` 48 | Note: if no commit is specified, vscode-bisect will automatically bisect the last 200 insider builds. Use '--releasedOnly' to only consider released builds and thus allow for older builds to be tested. 49 | 50 | Builds are stored and cached on disk in ${BUILD_FOLDER} 51 | `); 52 | 53 | const opts: Opts = program.parse(argv).opts(); 54 | 55 | if (opts.verbose) { 56 | LOGGER.verbose = true; 57 | } 58 | 59 | if (opts.reset) { 60 | try { 61 | console.log(`${chalk.gray('[build]')} deleting cache directory ${chalk.green(ROOT)}`); 62 | rmSync(ROOT, { recursive: true }); 63 | } catch (error) { } 64 | } 65 | 66 | if (opts.perf) { 67 | if (typeof opts.perf === 'string') { 68 | CONFIG.performance = resolve(opts.perf); 69 | if (await exists(CONFIG.performance)) { 70 | truncateSync(CONFIG.performance); 71 | } 72 | } else { 73 | CONFIG.performance = true; 74 | } 75 | 76 | if (opts.token && opts.runtime === 'vscode.dev') { 77 | CONFIG.token = opts.token; 78 | } 79 | 80 | if (opts.runtime !== 'vscode.dev') { 81 | await git.whenReady; 82 | } 83 | } 84 | 85 | let badCommitOrVersion = opts.bad; 86 | let goodCommitOrVersion = opts.good; 87 | if (!opts.commit && !opts.version) { 88 | if (!badCommitOrVersion) { 89 | const response = await prompts([ 90 | { 91 | type: 'text', 92 | name: 'bad', 93 | initial: '', 94 | message: 'Commit or version of released insiders build that reproduces the issue (leave empty to pick the latest build)', 95 | } 96 | ]); 97 | 98 | if (typeof response.bad === 'undefined') { 99 | process.exit(); 100 | } else if (response.bad) { 101 | badCommitOrVersion = response.bad; 102 | } 103 | } 104 | 105 | if (!goodCommitOrVersion) { 106 | const response = await prompts([ 107 | { 108 | type: 'text', 109 | name: 'good', 110 | initial: '', 111 | message: 'Commit or version of released insiders build that does not reproduce the issue (leave empty to pick the oldest build)', 112 | } 113 | ]); 114 | 115 | if (typeof response.good === 'undefined') { 116 | process.exit(); 117 | } else if (response.good) { 118 | goodCommitOrVersion = response.good; 119 | } 120 | } 121 | } 122 | 123 | try { 124 | let runtime: Runtime; 125 | if (opts.runtime === 'web') { 126 | runtime = Runtime.WebLocal; 127 | } else if (opts.runtime === 'vscode.dev') { 128 | runtime = Runtime.WebRemote; 129 | } else { 130 | runtime = Runtime.DesktopLocal; 131 | } 132 | 133 | let commit: string | undefined; 134 | if (opts.version) { 135 | if (!/^\d+\.\d+$/.test(opts.version)) { 136 | throw new Error(`Invalid version format. Please provide a version in the format of ${chalk.green('major.minor')}, for example ${chalk.green('1.93')}.`); 137 | } 138 | 139 | const build = await builds.fetchBuildByVersion(runtime, opts.version); 140 | commit = build.commit; 141 | } else if (opts.commit) { 142 | if (opts.commit === 'latest') { 143 | const allBuilds = await builds.fetchBuilds(runtime, undefined, undefined, opts.releasedOnly); 144 | commit = allBuilds[0].commit; 145 | } else { 146 | commit = opts.commit; 147 | } 148 | } 149 | 150 | // Commit provided: launch only that commit 151 | if (commit) { 152 | await bisecter.tryBuild({ commit, runtime }, { isBisecting: false, forceReDownload: false }); 153 | } 154 | 155 | // No commit provided: bisect commit ranges 156 | else { 157 | await bisecter.start(runtime, goodCommitOrVersion, badCommitOrVersion, opts.releasedOnly); 158 | } 159 | } catch (error) { 160 | console.log(`${chalk.red('\n[error]')} ${error}`); 161 | logTroubleshoot(); 162 | process.exit(1); 163 | } 164 | } -------------------------------------------------------------------------------- /src/launcher.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; 7 | import { join } from 'path'; 8 | import { URI } from 'vscode-uri'; 9 | import open from 'open'; 10 | import kill from 'tree-kill'; 11 | import { builds, IBuild } from './builds'; 12 | import { CONFIG, DATA_FOLDER, EXTENSIONS_FOLDER, GIT_VSCODE_FOLDER, LOGGER, DEFAULT_PERFORMANCE_FILE, Platform, platform, Runtime, USER_DATA_FOLDER, VSCODE_DEV_URL } from './constants'; 13 | import { mkdirSync, rmSync } from 'fs'; 14 | import { exists } from './files'; 15 | import chalk from 'chalk'; 16 | import * as perf from '@vscode/vscode-perf'; 17 | 18 | export interface IInstance { 19 | 20 | /** 21 | * Optional ellapsed time in milliseconds. 22 | * Only available for desktop builds and when 23 | * running with `--perf` command line flag. 24 | */ 25 | readonly ellapsed?: number; 26 | 27 | /** 28 | * Stops the instance. 29 | */ 30 | stop(): Promise; 31 | } 32 | 33 | const NOOP_INSTANCE: IInstance & IWebInstance = { stop: async () => { }, url: '' }; 34 | 35 | interface IWebInstance extends IInstance { 36 | 37 | /** 38 | * URL to the web instance. 39 | */ 40 | readonly url: string; 41 | } 42 | 43 | class Launcher { 44 | 45 | private static readonly WEB_AVAILABLE_REGEX = new RegExp('Web UI available at (http://localhost:8000/?\\?tkn=.+)'); 46 | 47 | static { 48 | 49 | // Recreate user data & extension folder 50 | try { 51 | rmSync(DATA_FOLDER, { recursive: true }); 52 | } catch (error) { } 53 | mkdirSync(DATA_FOLDER, { recursive: true }); 54 | } 55 | 56 | async launch(build: IBuild, options?: { forceReDownload: boolean }): Promise { 57 | 58 | // Install (unless web remote) 59 | if (build.runtime !== Runtime.WebRemote) { 60 | await builds.installBuild(build, options); 61 | } 62 | 63 | // Launch according to runtime 64 | switch (build.runtime) { 65 | 66 | // Web (local) 67 | case Runtime.WebLocal: 68 | if (CONFIG.performance) { 69 | console.log(`${chalk.gray('[build]')} starting local web build ${chalk.green(build.commit)} multiple times and measuring performance...`); 70 | return this.runWebPerformance(build); 71 | } 72 | 73 | console.log(`${chalk.gray('[build]')} starting local web build ${chalk.green(build.commit)}...`); 74 | return this.launchLocalWeb(build); 75 | 76 | // Web (remote) 77 | case Runtime.WebRemote: 78 | if (CONFIG.performance) { 79 | console.log(`${chalk.gray('[build]')} opening insiders.vscode.dev ${chalk.green(build.commit)} multiple times and measuring performance...`); 80 | return this.runWebPerformance(build); 81 | } 82 | 83 | console.log(`${chalk.gray('[build]')} opening insiders.vscode.dev ${chalk.green(build.commit)}...`); 84 | return this.launchRemoteWeb(build); 85 | 86 | // Desktop 87 | case Runtime.DesktopLocal: 88 | if (CONFIG.performance) { 89 | console.log(`${chalk.gray('[build]')} starting desktop build ${chalk.green(build.commit)} multiple times and measuring performance...`); 90 | return this.runDesktopPerformance(build); 91 | } 92 | 93 | console.log(`${chalk.gray('[build]')} starting desktop build ${chalk.green(build.commit)}...`); 94 | return this.launchElectron(build); 95 | } 96 | } 97 | 98 | private async runDesktopPerformance(build: IBuild): Promise { 99 | const executable = await this.getExecutablePath(build); 100 | 101 | await perf.run({ 102 | build: executable, 103 | folder: GIT_VSCODE_FOLDER, 104 | file: join(GIT_VSCODE_FOLDER, 'package.json'), 105 | profAppendTimers: typeof CONFIG.performance === 'string' ? CONFIG.performance : DEFAULT_PERFORMANCE_FILE 106 | }); 107 | 108 | return NOOP_INSTANCE; 109 | } 110 | 111 | private async runWebPerformance(build: IBuild): Promise { 112 | let url: string; 113 | let server: IWebInstance | undefined; 114 | 115 | // Web local: launch local web server 116 | if (build.runtime === Runtime.WebLocal) { 117 | server = await this.launchLocalWebServer(build); 118 | url = server.url; 119 | } 120 | 121 | // Web remote: use remote URL 122 | else { 123 | url = VSCODE_DEV_URL(build.commit); 124 | } 125 | 126 | try { 127 | await perf.run({ 128 | build: url, 129 | runtime: 'web', 130 | token: CONFIG.token, 131 | folder: build.runtime === Runtime.WebLocal ? URI.file(GIT_VSCODE_FOLDER).path /* supports Windows & POSIX */ : undefined, 132 | file: build.runtime === Runtime.WebLocal ? URI.file(join(GIT_VSCODE_FOLDER, 'package.json')).with({ scheme: 'vscode-remote', authority: 'localhost:9888' }).toString(true) : undefined, 133 | durationMarkersFile: typeof CONFIG.performance === 'string' ? CONFIG.performance : undefined, 134 | }); 135 | } finally { 136 | server?.stop(); 137 | } 138 | 139 | 140 | return NOOP_INSTANCE; 141 | } 142 | 143 | private async launchLocalWeb(build: IBuild): Promise { 144 | const instance = await this.launchLocalWebServer(build); 145 | if (instance.url) { 146 | open(instance.url); 147 | } 148 | 149 | return instance; 150 | } 151 | 152 | private async launchLocalWebServer(build: IBuild): Promise { 153 | const cp = await this.spawnBuild(build); 154 | 155 | async function stop() { 156 | return new Promise((resolve, reject) => { 157 | const pid = cp.pid!; 158 | kill(pid, error => { 159 | if (error) { 160 | try { 161 | process.kill(pid, 0); 162 | } catch (error) { 163 | resolve(); // process doesn't exist anymore... so, all good 164 | return; 165 | } 166 | 167 | reject(error); 168 | } else { 169 | resolve(); 170 | } 171 | }); 172 | }); 173 | } 174 | 175 | return new Promise(resolve => { 176 | cp.stdout.on('data', data => { 177 | if (LOGGER.verbose) { 178 | console.log(`${chalk.gray('[server]')}: ${data.toString()}`); 179 | } 180 | 181 | const matches = Launcher.WEB_AVAILABLE_REGEX.exec(data.toString()); 182 | const url = matches?.[1]; 183 | if (url) { 184 | resolve({ url, stop }); 185 | } 186 | }); 187 | 188 | cp.stderr.on('data', data => { 189 | if (LOGGER.verbose) { 190 | console.log(`${chalk.red('[server]')}: ${data.toString()}`); 191 | } 192 | }); 193 | }); 194 | } 195 | 196 | private async launchRemoteWeb(build: IBuild): Promise { 197 | open(VSCODE_DEV_URL(build.commit)); 198 | 199 | return NOOP_INSTANCE; 200 | } 201 | 202 | private async launchElectron(build: IBuild): Promise { 203 | const cp = await this.spawnBuild(build); 204 | 205 | async function stop() { 206 | cp.kill(); 207 | } 208 | 209 | cp.stdout.on('data', data => { 210 | if (LOGGER.verbose) { 211 | console.log(`${chalk.gray('[electron]')}: ${data.toString()}`); 212 | } 213 | }); 214 | 215 | cp.stderr.on('data', data => { 216 | if (LOGGER.verbose) { 217 | console.log(`${chalk.red('[electron]')}: ${data.toString()}`); 218 | } 219 | }); 220 | 221 | return { stop } 222 | } 223 | 224 | private async spawnBuild(build: IBuild): Promise { 225 | const executable = await this.getExecutablePath(build); 226 | if (LOGGER.verbose) { 227 | console.log(`${chalk.gray('[build]')} starting build via ${chalk.green(executable)}...`); 228 | } 229 | 230 | const args = [ 231 | '--accept-server-license-terms', 232 | '--extensions-dir', 233 | EXTENSIONS_FOLDER, 234 | '--skip-release-notes' 235 | ]; 236 | 237 | if (build.runtime === Runtime.DesktopLocal) { 238 | args.push( 239 | 240 | '--disable-updates', 241 | '--user-data-dir', 242 | USER_DATA_FOLDER, 243 | '--no-cached-data', 244 | '--disable-telemetry' // only disable telemetry when not running performance tests to be able to look at perf marks 245 | ); 246 | 247 | } 248 | 249 | switch (build.runtime) { 250 | case Runtime.WebLocal: 251 | case Runtime.WebRemote: 252 | switch (platform) { 253 | case Platform.MacOSX64: 254 | case Platform.MacOSArm: 255 | case Platform.LinuxX64: 256 | case Platform.LinuxArm: 257 | return spawn('bash', [executable, ...args]); 258 | case Platform.WindowsX64: 259 | case Platform.WindowsArm: 260 | return spawn(executable, args); 261 | } 262 | 263 | 264 | case Runtime.DesktopLocal: 265 | return spawn(executable, args); 266 | } 267 | } 268 | 269 | private async getExecutablePath(build: IBuild): Promise { 270 | const executable = await builds.getBuildExecutable(build); 271 | 272 | const executableExists = await exists(executable); 273 | if (!executableExists) { 274 | throw new Error(`[build] unable to find executable ${executable} on disk. Is the archive corrupt?`); 275 | } 276 | 277 | return executable; 278 | } 279 | } 280 | 281 | export const launcher = new Launcher(); -------------------------------------------------------------------------------- /src/storage.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { promises } from 'fs'; 7 | import { STORAGE_FILE } from './constants'; 8 | 9 | class Storage { 10 | 11 | private readonly whenReady = this.init(); 12 | 13 | private async init(): Promise<{ [key: string]: object }> { 14 | try { 15 | return JSON.parse((await promises.readFile(STORAGE_FILE)).toString()); 16 | } catch (error) { 17 | return {}; 18 | } 19 | } 20 | 21 | async store(key: string, value: T): Promise { 22 | const storage = await this.whenReady; 23 | 24 | // Add to in-memory 25 | storage[key] = value; 26 | 27 | // Persist on disk 28 | await promises.writeFile(STORAGE_FILE, JSON.stringify(storage)); 29 | } 30 | 31 | async getValue(key: string): Promise { 32 | const storage = await this.whenReady; 33 | 34 | return storage[key] as T | undefined; 35 | } 36 | } 37 | 38 | export const storage = new Storage(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "lib": ["es2020"], 6 | "rootDir": "./src", 7 | "outDir": "./out", 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "sourceMap": true 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------