Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27529bfc33 | ||
|
|
0e4ae83e74 | ||
|
|
3b1ff0f4a3 | ||
|
|
5079abd06f | ||
|
|
4e94f70e6f | ||
|
|
79e2666586 | ||
|
|
02080cd797 | ||
|
|
7347ff5512 | ||
|
|
c26217df88 | ||
|
|
31b445c8d2 | ||
|
|
7387ef6d2c | ||
|
|
091d36b1a0 | ||
|
|
292e695646 | ||
|
|
f154206b47 | ||
|
|
07eb334e6c | ||
|
|
89b86055d7 | ||
|
|
4dfec7014c | ||
|
|
fbff2df899 | ||
|
|
9cbe5ba2e8 | ||
|
|
70ddbd05be | ||
|
|
ace92a4674 | ||
|
|
24b3e158b7 | ||
|
|
a399041cba | ||
|
|
676546d32b | ||
|
|
a25db9616f | ||
|
|
cb4d6f228b | ||
|
|
424884b6b1 | ||
|
|
f741deb48b | ||
|
|
ae6be79c51 | ||
|
|
154b4a2fe2 | ||
|
|
650f4ca047 |
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@@ -30,6 +30,8 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
working-directory: extensions/ql-vscode
|
||||
env:
|
||||
APP_INSIGHTS_KEY: '${{ secrets.APP_INSIGHTS_KEY }}'
|
||||
run: |
|
||||
npm run build
|
||||
shell: bash
|
||||
@@ -71,6 +73,8 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
working-directory: extensions/ql-vscode
|
||||
env:
|
||||
APP_INSIGHTS_KEY: '${{ secrets.APP_INSIGHTS_KEY }}'
|
||||
run: |
|
||||
npm run build
|
||||
shell: bash
|
||||
@@ -121,7 +125,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
version: ['v2.2.6', 'v2.3.3', 'v2.4.0']
|
||||
version: ['v2.2.6', 'v2.3.3', 'v2.4.2']
|
||||
env:
|
||||
CLI_VERSION: ${{ matrix.version }}
|
||||
TEST_CODEQL_PATH: '${{ github.workspace }}/codeql'
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
||||
build:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
# TODO Share steps with the main workflow.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -36,7 +35,10 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
APP_INSIGHTS_KEY: '${{ secrets.APP_INSIGHTS_KEY }}'
|
||||
run: |
|
||||
echo "APP INSIGHTS KEY LENGTH: ${#APP_INSIGHTS_KEY}"
|
||||
cd extensions/ql-vscode
|
||||
npm run build -- --release
|
||||
shell: bash
|
||||
@@ -90,6 +92,10 @@ jobs:
|
||||
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
|
||||
asset_content_type: application/zip
|
||||
|
||||
###
|
||||
# Do Post release work: version bump and changelog PR
|
||||
# Only do this if we are running from a PR (ie- this is part of the release process)
|
||||
|
||||
# The checkout action does not fetch the main branch.
|
||||
# Fetch the main branch so that we can base the version bump PR against main.
|
||||
- name: Fetch main branch
|
||||
|
||||
@@ -94,7 +94,7 @@ Alternatively, you can run the tests inside of vscode. There are several vscode
|
||||
1. Double-check the `CHANGELOG.md` contains all desired change comments and has the version to be released with date at the top.
|
||||
* Go through all recent PRs and make sure they are properly accounted for.
|
||||
* Make sure all changelog entries have links back to their PR(s) if appropriate.
|
||||
1. Double-check that the extension `package.json` has the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
|
||||
1. Double-check that the extension `package.json` and `package-lock.json` have the version you intend to release. If you are doing a patch release (as opposed to minor or major version) this should already be correct.
|
||||
1. Create a PR for this release:
|
||||
* This PR will contain any missing bits from steps 1 and 2. Most of the time, this will just be updating `CHANGELOG.md` with today's date.
|
||||
* Create a new branch for the release named after the new version. For example: `v1.3.6`
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# CodeQL for Visual Studio Code: Changelog
|
||||
|
||||
## 1.4.2 - 2 February 2021
|
||||
|
||||
- Add a status bar item for the CodeQL CLI to show the current version. [#741](https://github.com/github/vscode-codeql/pull/741)
|
||||
- Fix version constraint for flagging CLI support of non-destructive updates. [#744](https://github.com/github/vscode-codeql/pull/744)
|
||||
- Add a _More Information_ button in the telemetry popup that opens [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md) in a browser tab. [#742](https://github.com/github/vscode-codeql/pull/742)
|
||||
|
||||
## 1.4.1 - 29 January 2021
|
||||
|
||||
- Reword the telemetry modal dialog box. [#738](https://github.com/github/vscode-codeql/pull/738)
|
||||
|
||||
## 1.4.0 - 29 January 2021
|
||||
|
||||
- Fix bug where databases are not reregistered when the query server restarts. [#734](https://github.com/github/vscode-codeql/pull/734)
|
||||
- Fix bug where upgrade requests were erroneously being marked as failed. [#734](https://github.com/github/vscode-codeql/pull/734)
|
||||
- On a strictly opt-in basis, collect anonymized usage data from the VS Code extension, helping improve CodeQL's usability and performance. See [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md) for more information on exactly what data is collected and what it is used for. [#611](https://github.com/github/vscode-codeql/pull/611)
|
||||
|
||||
## 1.3.10 - 20 January 2021
|
||||
|
||||
- Include the full stack in error log messages to help with debugging. [#726](https://github.com/github/vscode-codeql/pull/726)
|
||||
|
||||
@@ -110,3 +110,7 @@ For more information about the CodeQL extension, [see the documentation](https:/
|
||||
## License
|
||||
|
||||
The CodeQL extension for Visual Studio Code is [licensed](LICENSE.md) under the MIT License. The version of CodeQL used by the CodeQL extension is subject to the [GitHub CodeQL Terms & Conditions](https://securitylab.github.com/tools/codeql/license).
|
||||
|
||||
## Data and Telemetry
|
||||
|
||||
If you specifically opt-in to permit GitHub to do so, GitHub will collect usage data and metrics for the purposes of helping the core developers to improve the CodeQL extension for VS Code. This data will not be shared with any parties outside of GitHub. IP addresses and installation IDs will be retained for a maximum of 30 days. Anonymous data will be retained for a maximum of 180 days. Please see [telemetry](TELEMETRY.md) for more information.
|
||||
|
||||
47
extensions/ql-vscode/TELEMETRY.md
Normal file
47
extensions/ql-vscode/TELEMETRY.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Telemetry in the CodeQL extension for VS Code
|
||||
|
||||
If you specifically opt-in to permit GitHub to do so, GitHub will collect usage data and metrics for the purposes of helping the core developers to improve the CodeQL extension for VS Code. This data will not be shared with any parties outside of GitHub. IP addresses and installation IDs will be retained for a maximum of 30 days. Anonymous data will be retained for a maximum of 180 days.
|
||||
|
||||
## Why do you collect data?
|
||||
|
||||
GitHub collects aggregated, anonymous usage data and metrics to help us improve CodeQL for VS Code. IP addresses and installation IDs are collected only to ensure that anonymous data is not duplicated during aggregation.
|
||||
|
||||
## What data is collected
|
||||
|
||||
If you opt in, GitHub collects the following information related to the usage of the extension. The data collected are:
|
||||
|
||||
- The identifiers of any CodeQL-related [VS Code commands](https://code.visualstudio.com/docs/getstarted/tips-and-tricks#_command-palette) that are run
|
||||
- For each command: the timestamp, time taken, and whether or not the command completed successfully
|
||||
- VS Code and extension version
|
||||
- Randomly generated GUID that uniquely identifies a CodeQL extension installation. (Discarded before aggregation.)
|
||||
- IP address of the client sending the telemetry data. (Discarded before aggregation.)
|
||||
- Whether or not the `codeQL.canary` setting is enabled and set to `true`
|
||||
|
||||
## How long will data be retained?
|
||||
|
||||
IP address and GUIDs will be retained for a maximum of 30 days. Anonymous, aggregated data that includes command identifiers, run times, and timestamps will be retained for a maximum of 180 days.
|
||||
|
||||
## Who will have access to this data?
|
||||
|
||||
IP address and GUIDs will only be available to the core developers of CodeQL. Aggregated data will be available to GitHub employees.
|
||||
|
||||
## What data is **NOT** collected?
|
||||
|
||||
We only collect the minimal amount of data we need to answer the questions about how our users are experiencing this product. To that end, we do not collect the following information:
|
||||
|
||||
- No GitHub user ID
|
||||
- No CodeQL database names or contents
|
||||
- No contents of CodeQL queries
|
||||
- No filesystem paths.
|
||||
|
||||
## How do I disable telemetry reporting?
|
||||
|
||||
When telemetry collection is disabled, no data will be sent to GitHub servers.
|
||||
|
||||
You can disable telemetry collection by setting `codeQL.telemetry.enableTelemetry` to `false` in [your settings](https://code.visualstudio.com/docs/getstarted/settings#_settings-editor). Telemetry collection is _disabled_ by default.
|
||||
|
||||
Additionally, telemetry collection will be disabled if the global `telemetry.enableTelemetry` setting is set to `false`. For more information on global telemetry collection, see [Microsoft’s documentation](https://code.visualstudio.com/docs/supporting/faq#_how-to-disable-telemetry-reporting).
|
||||
|
||||
## More information
|
||||
|
||||
See GitHub's [Privacy Statement](https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-privacy-statement) and [Terms of Service](https://docs.github.com/en/free-pro-team@latest/github/site-policy/github-terms-of-service) for more information.
|
||||
16
extensions/ql-vscode/gulpfile.ts/appInsights.ts
Normal file
16
extensions/ql-vscode/gulpfile.ts/appInsights.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as gulp from 'gulp';
|
||||
import * as replace from 'gulp-replace';
|
||||
|
||||
/** Inject the application insights key into the telemetry file */
|
||||
export function injectAppInsightsKey() {
|
||||
if (!process.env.APP_INSIGHTS_KEY) {
|
||||
// noop
|
||||
console.log('APP_INSIGHTS_KEY environment variable is not set. So, cannot inject it into the application.');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// replace the key
|
||||
return gulp.src(['out/telemetry.js'])
|
||||
.pipe(replace(/REPLACE-APP-INSIGHTS-KEY/, process.env.APP_INSIGHTS_KEY))
|
||||
.pipe(gulp.dest('out/'));
|
||||
}
|
||||
@@ -4,7 +4,12 @@ import { compileTextMateGrammar } from './textmate';
|
||||
import { copyTestData } from './tests';
|
||||
import { compileView } from './webpack';
|
||||
import { packageExtension } from './package';
|
||||
import { injectAppInsightsKey } from './appInsights';
|
||||
|
||||
export const buildWithoutPackage = gulp.parallel(compileTypeScript, compileTextMateGrammar, compileView, copyTestData, copyViewCss);
|
||||
export { compileTextMateGrammar, watchTypeScript, compileTypeScript, copyTestData };
|
||||
exports.default = gulp.series(exports.buildWithoutPackage, packageExtension);
|
||||
export const buildWithoutPackage =
|
||||
gulp.parallel(
|
||||
compileTypeScript, compileTextMateGrammar, compileView, copyTestData, copyViewCss
|
||||
);
|
||||
|
||||
export { compileTextMateGrammar, watchTypeScript, compileTypeScript, copyTestData, injectAppInsightsKey };
|
||||
export default gulp.series(buildWithoutPackage, injectAppInsightsKey, packageExtension);
|
||||
|
||||
BIN
extensions/ql-vscode/media/canary-logo.png
Normal file
BIN
extensions/ql-vscode/media/canary-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
204
extensions/ql-vscode/package-lock.json
generated
204
extensions/ql-vscode/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vscode-codeql",
|
||||
"version": "1.3.9",
|
||||
"version": "1.4.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -268,6 +268,15 @@
|
||||
"chokidar": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"@types/gulp-replace": {
|
||||
"version": "0.0.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/gulp-replace/-/gulp-replace-0.0.31.tgz",
|
||||
"integrity": "sha512-dbgQ1u0N9ShXrzahBgQfMSu6qUh8nlTLt7whhQ0S0sEUHhV3scysppJ1UX0fl53PJENgAL99ueykddyrCaDt7g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/gulp-sourcemaps": {
|
||||
"version": "0.0.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/gulp-sourcemaps/-/gulp-sourcemaps-0.0.32.tgz",
|
||||
@@ -952,6 +961,18 @@
|
||||
"buffer-equal": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"applicationinsights": {
|
||||
"version": "1.8.7",
|
||||
"resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.8.7.tgz",
|
||||
"integrity": "sha512-+HENzPBdSjnWL9mc+9o+j9pEaVNI4WsH5RNvfmRLfwQYvbJumcBi4S5bUzclug5KCcFP0S4bYJOmm9MV3kv2GA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cls-hooked": "^4.2.2",
|
||||
"continuation-local-storage": "^3.2.1",
|
||||
"diagnostic-channel": "0.3.1",
|
||||
"diagnostic-channel-publishers": "0.4.1"
|
||||
}
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
@@ -1230,6 +1251,30 @@
|
||||
"integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==",
|
||||
"dev": true
|
||||
},
|
||||
"async-hook-jl": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz",
|
||||
"integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==",
|
||||
"requires": {
|
||||
"stack-chain": "^1.3.7"
|
||||
}
|
||||
},
|
||||
"async-listener": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/async-listener/-/async-listener-0.6.10.tgz",
|
||||
"integrity": "sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==",
|
||||
"requires": {
|
||||
"semver": "^5.3.0",
|
||||
"shimmer": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"async-settle": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz",
|
||||
@@ -1376,6 +1421,12 @@
|
||||
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
|
||||
"dev": true
|
||||
},
|
||||
"binaryextensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-2.3.0.tgz",
|
||||
"integrity": "sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg==",
|
||||
"dev": true
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
@@ -2024,6 +2075,23 @@
|
||||
"readable-stream": "^2.3.5"
|
||||
}
|
||||
},
|
||||
"cls-hooked": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz",
|
||||
"integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==",
|
||||
"requires": {
|
||||
"async-hook-jl": "^1.7.6",
|
||||
"emitter-listener": "^1.0.1",
|
||||
"semver": "^5.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
@@ -2151,6 +2219,15 @@
|
||||
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=",
|
||||
"dev": true
|
||||
},
|
||||
"continuation-local-storage": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz",
|
||||
"integrity": "sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==",
|
||||
"requires": {
|
||||
"async-listener": "^0.6.0",
|
||||
"emitter-listener": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"convert-source-map": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
|
||||
@@ -2599,6 +2676,29 @@
|
||||
"integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=",
|
||||
"dev": true
|
||||
},
|
||||
"diagnostic-channel": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.3.1.tgz",
|
||||
"integrity": "sha512-6eb9YRrimz8oTr5+JDzGmSYnXy5V7YnK5y/hd8AUDK1MssHjQKm9LlD6NSrHx4vMDF3+e/spI2hmWTviElgWZA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostic-channel-publishers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.4.1.tgz",
|
||||
"integrity": "sha512-NpZ7IOVUfea/kAx4+ub4NIYZyRCSymjXM5BZxnThs3ul9gAKqjm7J8QDDQW3Ecuo2XxjNLoWLeKmrPUWKNZaYw==",
|
||||
"dev": true
|
||||
},
|
||||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -2704,6 +2804,12 @@
|
||||
"object.defaults": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"editions": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz",
|
||||
"integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==",
|
||||
"dev": true
|
||||
},
|
||||
"editorconfig": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
|
||||
@@ -2753,6 +2859,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"emitter-listener": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
|
||||
"integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
|
||||
"requires": {
|
||||
"shimmer": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -4011,6 +4125,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"gulp-replace": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-1.0.0.tgz",
|
||||
"integrity": "sha512-lgdmrFSI1SdhNMXZQbrC75MOl1UjYWlOWNbNRnz+F/KHmgxt3l6XstBoAYIdadwETFyG/6i+vWUSCawdC3pqOw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"istextorbinary": "2.2.1",
|
||||
"readable-stream": "^2.0.1",
|
||||
"replacestream": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"gulp-sourcemaps": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.5.tgz",
|
||||
@@ -4904,6 +5029,17 @@
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||
"dev": true
|
||||
},
|
||||
"istextorbinary": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.2.1.tgz",
|
||||
"integrity": "sha512-TS+hoFl8Z5FAFMK38nhBkdLt44CclNRgDHWeMgsV8ko3nDlr/9UI2Sf839sW7enijf8oKsZYXRvM8g0it9Zmcw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binaryextensions": "2",
|
||||
"editions": "^1.3.3",
|
||||
"textextensions": "2"
|
||||
}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -7547,6 +7683,17 @@
|
||||
"remove-trailing-separator": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"replacestream": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz",
|
||||
"integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^1.0.3",
|
||||
"object-assign": "^4.0.1",
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -7814,6 +7961,11 @@
|
||||
"integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==",
|
||||
"dev": true
|
||||
},
|
||||
"shimmer": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
|
||||
"integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz",
|
||||
@@ -8122,6 +8274,11 @@
|
||||
"figgy-pudding": "^3.5.1"
|
||||
}
|
||||
},
|
||||
"stack-chain": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz",
|
||||
"integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU="
|
||||
},
|
||||
"stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
@@ -8505,6 +8662,12 @@
|
||||
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
|
||||
"dev": true
|
||||
},
|
||||
"textextensions": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/textextensions/-/textextensions-2.6.0.tgz",
|
||||
"integrity": "sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
@@ -9211,6 +9374,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vscode-extension-telemetry": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.6.tgz",
|
||||
"integrity": "sha512-rbzSg7k4NnsCdF4Lz0gI4jl3JLXR0hnlmfFgsY8CSDYhXgdoIxcre8jw5rjkobY0xhSDhbG7xCjP8zxskySJ/g==",
|
||||
"requires": {
|
||||
"applicationinsights": "1.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"applicationinsights": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.7.4.tgz",
|
||||
"integrity": "sha512-XFLsNlcanpjFhHNvVWEfcm6hr7lu9znnb6Le1Lk5RE03YUV9X2B2n2MfM4kJZRrUdV+C0hdHxvWyv+vWoLfY7A==",
|
||||
"requires": {
|
||||
"cls-hooked": "^4.2.2",
|
||||
"continuation-local-storage": "^3.2.1",
|
||||
"diagnostic-channel": "0.2.0",
|
||||
"diagnostic-channel-publishers": "^0.3.3"
|
||||
}
|
||||
},
|
||||
"diagnostic-channel": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz",
|
||||
"integrity": "sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=",
|
||||
"requires": {
|
||||
"semver": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"diagnostic-channel-publishers": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.5.tgz",
|
||||
"integrity": "sha512-AOIjw4T7Nxl0G2BoBPhkQ6i7T4bUd9+xvdYizwvG7vVAM1dvr+SDrcUudlmzwH0kbEwdR2V1EcnKT0wAeYLQNQ=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"vscode-jsonrpc": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "CodeQL for Visual Studio Code",
|
||||
"author": "GitHub",
|
||||
"private": true,
|
||||
"version": "1.3.10",
|
||||
"version": "1.4.2",
|
||||
"publisher": "GitHub",
|
||||
"license": "MIT",
|
||||
"icon": "media/VS-marketplace-CodeQL-icon.png",
|
||||
@@ -174,6 +174,18 @@
|
||||
"minimum": 0,
|
||||
"maximum": 1024,
|
||||
"description": "Number of threads for running CodeQL tests."
|
||||
},
|
||||
"codeQL.telemetry.enableTelemetry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"markdownDescription": "Specifies whether to send CodeQL usage telemetry. This setting AND the global `#telemetry.enableTelemetry#` setting must be checked for telemetry to be sent to GitHub. For more information, see [TELEMETRY.md](https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md)"
|
||||
},
|
||||
"codeQL.telemetry.logTelemetry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"scope": "application",
|
||||
"description": "Specifies whether or not to write telemetry events to the extension log."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -194,6 +206,10 @@
|
||||
"command": "codeQL.quickQuery",
|
||||
"title": "CodeQL: Quick Query"
|
||||
},
|
||||
{
|
||||
"command": "codeQL.openDocumentation",
|
||||
"title": "CodeQL: Open Documentation"
|
||||
},
|
||||
{
|
||||
"command": "codeQLDatabases.chooseDatabaseFolder",
|
||||
"title": "Choose Database from Folder",
|
||||
@@ -735,6 +751,7 @@
|
||||
"tmp-promise": "~3.0.2",
|
||||
"tree-kill": "~1.2.2",
|
||||
"unzipper": "~0.10.5",
|
||||
"vscode-extension-telemetry": "^0.1.6",
|
||||
"vscode-jsonrpc": "^5.0.1",
|
||||
"vscode-languageclient": "^6.1.3",
|
||||
"vscode-test-adapter-api": "~1.7.0",
|
||||
@@ -750,6 +767,7 @@
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/google-protobuf": "^3.2.7",
|
||||
"@types/gulp": "^4.0.6",
|
||||
"@types/gulp-replace": "0.0.31",
|
||||
"@types/gulp-sourcemaps": "0.0.32",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jszip": "~3.1.6",
|
||||
@@ -772,6 +790,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "~2.23.0",
|
||||
"@typescript-eslint/parser": "~2.23.0",
|
||||
"ansi-colors": "^4.1.1",
|
||||
"applicationinsights": "^1.8.7",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "~7.1.1",
|
||||
"css-loader": "~3.1.0",
|
||||
@@ -779,6 +798,7 @@
|
||||
"eslint-plugin-react": "~7.19.0",
|
||||
"glob": "^7.1.4",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-replace": "^1.0.0",
|
||||
"gulp-sourcemaps": "^2.6.5",
|
||||
"gulp-typescript": "^5.0.1",
|
||||
"husky": "~4.2.5",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { promisify } from 'util';
|
||||
import { CancellationToken, Disposable } from 'vscode';
|
||||
|
||||
import { BQRSInfo, DecodedBqrsChunk } from './pure/bqrs-cli-types';
|
||||
import * as config from './config';
|
||||
import { CliConfig } from './config';
|
||||
import { DistributionProvider, FindDistributionResultKind } from './distribution';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
@@ -59,6 +60,7 @@ export interface DbInfo {
|
||||
export interface UpgradesInfo {
|
||||
scripts: string[];
|
||||
finalDbscheme: string;
|
||||
matchesTarget?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,7 +575,7 @@ export class CodeQLCliServer implements Disposable {
|
||||
return await this.runJsonCodeQlCliCommand<DecodedBqrsChunk>(['bqrs', 'decode'], subcommandArgs, 'Reading bqrs data');
|
||||
}
|
||||
|
||||
async interpretBqrs(metadata: { kind: string; id: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
|
||||
async interpretBqrs(metadata: { kind: string; id: string; scored?: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
|
||||
const args = [
|
||||
`-t=kind=${metadata.kind}`,
|
||||
`-t=id=${metadata.id}`,
|
||||
@@ -585,6 +587,9 @@ export class CodeQLCliServer implements Disposable {
|
||||
// grouping client-side.
|
||||
'--no-group-results',
|
||||
];
|
||||
if (config.isCanary() && metadata.scored !== undefined) {
|
||||
args.push(`-t=scored=${metadata.scored}`);
|
||||
}
|
||||
if (sourceInfo !== undefined) {
|
||||
args.push(
|
||||
'--source-archive', sourceInfo.sourceArchive,
|
||||
@@ -646,11 +651,14 @@ export class CodeQLCliServer implements Disposable {
|
||||
* Gets information necessary for upgrading a database.
|
||||
* @param dbScheme the path to the dbscheme of the database to be upgraded.
|
||||
* @param searchPath A list of directories to search for upgrade scripts.
|
||||
* @param targetDbScheme The dbscheme to try to upgrade to.
|
||||
* @returns A list of database upgrade script directories
|
||||
*/
|
||||
resolveUpgrades(dbScheme: string, searchPath: string[]): Promise<UpgradesInfo> {
|
||||
resolveUpgrades(dbScheme: string, searchPath: string[], targetDbScheme?: string): Promise<UpgradesInfo> {
|
||||
const args = ['--additional-packs', searchPath.join(path.delimiter), '--dbscheme', dbScheme];
|
||||
|
||||
if (targetDbScheme) {
|
||||
args.push('--target-dbscheme', targetDbScheme);
|
||||
}
|
||||
return this.runJsonCodeQlCliCommand<UpgradesInfo>(
|
||||
['resolve', 'upgrades'],
|
||||
args,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'vscode';
|
||||
import { showAndLogErrorMessage, showAndLogWarningMessage } from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { telemetryListener } from './telemetry';
|
||||
|
||||
export class UserCancellationException extends Error {
|
||||
/**
|
||||
@@ -114,9 +115,13 @@ export function commandRunner(
|
||||
task: NoProgressTask,
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
let error: Error | undefined;
|
||||
|
||||
try {
|
||||
return await task(...args);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
const errorMessage = `${e.message || e} (${commandId})`;
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
@@ -135,6 +140,9 @@ export function commandRunner(
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -155,6 +163,8 @@ export function commandRunnerWithProgress<R>(
|
||||
progressOptions: Partial<ProgressOptions>
|
||||
): Disposable {
|
||||
return commands.registerCommand(commandId, async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
let error: Error | undefined;
|
||||
const progressOptionsWithDefaults = {
|
||||
location: ProgressLocation.Notification,
|
||||
...progressOptions
|
||||
@@ -162,6 +172,7 @@ export function commandRunnerWithProgress<R>(
|
||||
try {
|
||||
return await withProgress(progressOptionsWithDefaults, task, ...args);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
const errorMessage = `${e.message || e} (${commandId})`;
|
||||
if (e instanceof UserCancellationException) {
|
||||
// User has cancelled this action manually
|
||||
@@ -180,6 +191,9 @@ export function commandRunnerWithProgress<R>(
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
} finally {
|
||||
const executionTime = Date.now() - startTime;
|
||||
telemetryListener.sendCommandUsage(commandId, executionTime, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DistributionManager } from './distribution';
|
||||
import { logger } from './logging';
|
||||
|
||||
/** Helper class to look up a labelled (and possibly nested) setting. */
|
||||
class Setting {
|
||||
export class Setting {
|
||||
name: string;
|
||||
parent?: Setting;
|
||||
|
||||
@@ -39,8 +39,16 @@ class Setting {
|
||||
|
||||
const ROOT_SETTING = new Setting('codeQL');
|
||||
|
||||
// Distribution configuration
|
||||
// Global configuration
|
||||
const TELEMETRY_SETTING = new Setting('telemetry', ROOT_SETTING);
|
||||
const GLOBAL_TELEMETRY_SETTING = new Setting('telemetry');
|
||||
|
||||
export const LOG_TELEMETRY = new Setting('logTelemetry', TELEMETRY_SETTING);
|
||||
export const ENABLE_TELEMETRY = new Setting('enableTelemetry', TELEMETRY_SETTING);
|
||||
|
||||
export const GLOBAL_ENABLE_TELEMETRY = new Setting('enableTelemetry', GLOBAL_TELEMETRY_SETTING);
|
||||
|
||||
// Distribution configuration
|
||||
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
|
||||
const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
|
||||
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
|
||||
@@ -104,7 +112,7 @@ export interface CliConfig {
|
||||
}
|
||||
|
||||
|
||||
abstract class ConfigListener extends DisposableObject {
|
||||
export abstract class ConfigListener extends DisposableObject {
|
||||
protected readonly _onDidChangeConfiguration = this.push(new EventEmitter<void>());
|
||||
|
||||
constructor() {
|
||||
@@ -240,3 +248,12 @@ export class CliConfigListener extends ConfigListener implements CliConfig {
|
||||
* want to enable experimental features, they can add them directly in
|
||||
* their vscode settings json file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Enables canary features of this extension. Recommended for all internal users.
|
||||
*/
|
||||
export const CANARY_FEATURES = new Setting('canary', ROOT_SETTING);
|
||||
|
||||
export function isCanary() {
|
||||
return !!CANARY_FEATURES.getValue<boolean>();
|
||||
}
|
||||
|
||||
@@ -12,12 +12,10 @@ import {
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import * as cli from './cli';
|
||||
import {
|
||||
DatabaseChangedEvent,
|
||||
DatabaseItem,
|
||||
DatabaseManager,
|
||||
getUpgradesDirectories,
|
||||
} from './databases';
|
||||
import {
|
||||
commandRunner,
|
||||
@@ -25,15 +23,14 @@ import {
|
||||
ProgressCallback,
|
||||
} from './commandRunner';
|
||||
import {
|
||||
getOnDiskWorkspaceFolders,
|
||||
showAndLogErrorMessage,
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDbLanguageFolder
|
||||
isLikelyDbLanguageFolder,
|
||||
showAndLogErrorMessage
|
||||
} from './helpers';
|
||||
import { logger } from './logging';
|
||||
import { clearCacheInDatabase } from './run-queries';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { upgradeDatabase } from './upgrades';
|
||||
import { upgradeDatabaseExplicit } from './upgrades';
|
||||
import {
|
||||
importArchiveDatabase,
|
||||
promptImportInternetDatabase,
|
||||
@@ -218,7 +215,6 @@ export class DatabaseUI extends DisposableObject {
|
||||
private treeDataProvider: DatabaseTreeDataProvider;
|
||||
|
||||
public constructor(
|
||||
private cliserver: cli.CodeQLCliServer,
|
||||
private databaseManager: DatabaseManager,
|
||||
private readonly queryServer: qsClient.QueryServerClient | undefined,
|
||||
private readonly storagePath: string,
|
||||
@@ -540,25 +536,10 @@ export class DatabaseUI extends DisposableObject {
|
||||
}
|
||||
|
||||
// Search for upgrade scripts in any workspace folders available
|
||||
const searchPath: string[] = getOnDiskWorkspaceFolders();
|
||||
|
||||
const upgradeInfo = await this.cliserver.resolveUpgrades(
|
||||
databaseItem.contents.dbSchemeUri.fsPath,
|
||||
searchPath
|
||||
);
|
||||
|
||||
const { scripts, finalDbscheme } = upgradeInfo;
|
||||
|
||||
if (finalDbscheme === undefined) {
|
||||
throw new Error('Could not determine target dbscheme to upgrade to.');
|
||||
}
|
||||
const targetDbSchemeUri = Uri.file(finalDbscheme);
|
||||
|
||||
await upgradeDatabase(
|
||||
await upgradeDatabaseExplicit(
|
||||
this.queryServer,
|
||||
databaseItem,
|
||||
targetDbSchemeUri,
|
||||
getUpgradesDirectories(scripts),
|
||||
progress,
|
||||
token
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
showAndLogErrorMessage,
|
||||
showAndLogWarningMessage,
|
||||
showAndLogInformationMessage,
|
||||
isLikelyDatabaseRoot,
|
||||
isLikelyDatabaseRoot
|
||||
} from './helpers';
|
||||
import {
|
||||
ProgressCallback,
|
||||
@@ -514,7 +514,10 @@ export class DatabaseManager extends DisposableObject {
|
||||
) {
|
||||
super();
|
||||
|
||||
this.loadPersistedState(); // Let this run async.
|
||||
qs.onDidStartQueryServer(this.reregisterDatabases.bind(this));
|
||||
|
||||
// Let this run async.
|
||||
this.loadPersistedState();
|
||||
}
|
||||
|
||||
public async openDatabase(
|
||||
@@ -542,6 +545,22 @@ export class DatabaseManager extends DisposableObject {
|
||||
return databaseItem;
|
||||
}
|
||||
|
||||
private async reregisterDatabases(
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken
|
||||
) {
|
||||
let completed = 0;
|
||||
await Promise.all(this._databaseItems.map(async (databaseItem) => {
|
||||
await this.registerDatabase(progress, token, databaseItem);
|
||||
completed++;
|
||||
progress({
|
||||
maxStep: this._databaseItems.length,
|
||||
step: completed,
|
||||
message: 'Re-registering databases'
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
private async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
|
||||
// The folder may already be in workspace state from a previous
|
||||
// session. If not, add it.
|
||||
|
||||
@@ -225,9 +225,11 @@ export class DistributionManager implements DistributionProvider {
|
||||
*
|
||||
* Returns a failed promise if an unexpected error occurs during installation.
|
||||
*/
|
||||
public installExtensionManagedDistributionRelease(release: Release,
|
||||
progressCallback?: ProgressCallback): Promise<void> {
|
||||
return this.extensionSpecificDistributionManager!.installDistributionRelease(release, progressCallback);
|
||||
public installExtensionManagedDistributionRelease(
|
||||
release: Release,
|
||||
progressCallback?: ProgressCallback
|
||||
): Promise<void> {
|
||||
return this.extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
|
||||
}
|
||||
|
||||
public get onDidChangeDistribution(): Event<void> | undefined {
|
||||
|
||||
@@ -45,7 +45,6 @@ import {
|
||||
GithubRateLimitedError
|
||||
} from './distribution';
|
||||
import * as helpers from './helpers';
|
||||
import { commandRunner, commandRunnerWithProgress, ProgressCallback, ProgressUpdate, withProgress } from './commandRunner';
|
||||
import { assertNever } from './pure/helpers-pure';
|
||||
import { spawnIdeServer } from './ide-server';
|
||||
import { InterfaceManager } from './interface';
|
||||
@@ -60,6 +59,15 @@ import { QLTestAdapterFactory } from './test-adapter';
|
||||
import { TestUIService } from './test-ui';
|
||||
import { CompareInterfaceManager } from './compare/compare-interface';
|
||||
import { gatherQlFiles } from './pure/files';
|
||||
import { initializeTelemetry } from './telemetry';
|
||||
import {
|
||||
commandRunner,
|
||||
commandRunnerWithProgress,
|
||||
ProgressCallback,
|
||||
withProgress,
|
||||
ProgressUpdate
|
||||
} from './commandRunner';
|
||||
import { CodeQlStatusBarHandler } from './status-bar';
|
||||
|
||||
/**
|
||||
* extension.ts
|
||||
@@ -88,6 +96,9 @@ const errorStubs: Disposable[] = [];
|
||||
*/
|
||||
let isInstallingOrUpdatingDistribution = false;
|
||||
|
||||
const extensionId = 'GitHub.vscode-codeql';
|
||||
const extension = extensions.getExtension(extensionId);
|
||||
|
||||
/**
|
||||
* If the user tries to execute vscode commands after extension activation is failed, give
|
||||
* a sensible error message.
|
||||
@@ -98,8 +109,6 @@ function registerErrorStubs(excludedCommands: string[], stubGenerator: (command:
|
||||
// Remove existing stubs
|
||||
errorStubs.forEach(stub => stub.dispose());
|
||||
|
||||
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
|
||||
const extension = extensions.getExtension(extensionId);
|
||||
if (extension === undefined) {
|
||||
throw new Error(`Can't find extension ${extensionId}`);
|
||||
}
|
||||
@@ -139,10 +148,14 @@ export interface CodeQLExtensionInterface {
|
||||
* @returns CodeQLExtensionInterface
|
||||
*/
|
||||
export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionInterface | {}> {
|
||||
logger.log('Starting CodeQL extension');
|
||||
logger.log(`Starting ${extensionId} extension`);
|
||||
if (extension === undefined) {
|
||||
throw new Error(`Can't find extension ${extensionId}`);
|
||||
}
|
||||
|
||||
const distributionConfigListener = new DistributionConfigListener();
|
||||
initializeLogging(ctx);
|
||||
await initializeTelemetry(extension, ctx);
|
||||
languageSupport.install();
|
||||
|
||||
ctx.subscriptions.push(distributionConfigListener);
|
||||
@@ -278,14 +291,22 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
return result;
|
||||
}
|
||||
|
||||
async function installOrUpdateThenTryActivate(config: DistributionUpdateConfig): Promise<CodeQLExtensionInterface | {}> {
|
||||
async function installOrUpdateThenTryActivate(
|
||||
config: DistributionUpdateConfig
|
||||
): Promise<CodeQLExtensionInterface | {}> {
|
||||
|
||||
await installOrUpdateDistribution(config);
|
||||
|
||||
// Display the warnings even if the extension has already activated.
|
||||
const distributionResult = await getDistributionDisplayingDistributionWarnings();
|
||||
let extensionInterface: CodeQLExtensionInterface | {} = {};
|
||||
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
|
||||
extensionInterface = await activateWithInstalledDistribution(ctx, distributionManager);
|
||||
extensionInterface = await activateWithInstalledDistribution(
|
||||
ctx,
|
||||
distributionManager,
|
||||
distributionConfigListener
|
||||
);
|
||||
|
||||
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
|
||||
registerErrorStubs([checkForUpdatesCommand], command => async () => {
|
||||
const installActionName = 'Install CodeQL CLI';
|
||||
@@ -327,7 +348,8 @@ export async function activate(ctx: ExtensionContext): Promise<CodeQLExtensionIn
|
||||
|
||||
async function activateWithInstalledDistribution(
|
||||
ctx: ExtensionContext,
|
||||
distributionManager: DistributionManager
|
||||
distributionManager: DistributionManager,
|
||||
distributionConfigListener: DistributionConfigListener
|
||||
): Promise<CodeQLExtensionInterface> {
|
||||
beganMainExtensionActivation = true;
|
||||
// Remove any error stubs command handlers left over from first part
|
||||
@@ -348,6 +370,9 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
ctx.subscriptions.push(cliServer);
|
||||
|
||||
const statusBar = new CodeQlStatusBarHandler(cliServer, distributionConfigListener);
|
||||
ctx.subscriptions.push(statusBar);
|
||||
|
||||
logger.log('Initializing query server client.');
|
||||
const qs = new qsClient.QueryServerClient(
|
||||
qlConfigurationListener,
|
||||
@@ -369,7 +394,6 @@ async function activateWithInstalledDistribution(
|
||||
ctx.subscriptions.push(dbm);
|
||||
logger.log('Initializing database panel.');
|
||||
const databaseUI = new DatabaseUI(
|
||||
cliServer,
|
||||
dbm,
|
||||
qs,
|
||||
getContextStoragePath(ctx),
|
||||
@@ -595,28 +619,36 @@ async function activateWithInstalledDistribution(
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.restartQueryServer', async () => {
|
||||
await qs.restartQueryServer();
|
||||
commandRunnerWithProgress('codeQL.restartQueryServer', async (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
await qs.restartQueryServer(progress, token);
|
||||
helpers.showAndLogInformationMessage('CodeQL Query Server restarted.', {
|
||||
outputLogger: queryServerLogger,
|
||||
});
|
||||
}, {
|
||||
title: 'Restarting Query Server'
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress('codeQL.chooseDatabaseFolder', (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) =>
|
||||
databaseUI.handleChooseDatabaseFolder(progress, token), {
|
||||
title: 'Choose a Database from a Folder'
|
||||
})
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.chooseDatabaseFolder', (
|
||||
commandRunnerWithProgress('codeQL.chooseDatabaseArchive', (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) =>
|
||||
databaseUI.handleChooseDatabaseFolder(progress, token)
|
||||
)
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.chooseDatabaseArchive', (
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
) =>
|
||||
databaseUI.handleChooseDatabaseArchive(progress, token)
|
||||
)
|
||||
databaseUI.handleChooseDatabaseArchive(progress, token), {
|
||||
title: 'Choose a Database from an Archive'
|
||||
})
|
||||
);
|
||||
ctx.subscriptions.push(
|
||||
commandRunnerWithProgress('codeQL.chooseDatabaseLgtm', (
|
||||
@@ -640,6 +672,10 @@ async function activateWithInstalledDistribution(
|
||||
})
|
||||
);
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.openDocumentation', async () =>
|
||||
env.openExternal(Uri.parse('https://codeql.github.com/docs/'))));
|
||||
|
||||
logger.log('Starting language server.');
|
||||
ctx.subscriptions.push(client.start());
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import * as yaml from 'js-yaml';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
ExtensionContext,
|
||||
Uri,
|
||||
window as Window,
|
||||
workspace
|
||||
workspace,
|
||||
env
|
||||
} from 'vscode';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { logger } from './logging';
|
||||
@@ -80,17 +82,61 @@ async function internalShowAndLog(
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a yes/no choice.
|
||||
* @param message The message to show.
|
||||
*
|
||||
* @return `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
|
||||
* @param message The message to show.
|
||||
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
|
||||
* be closed even if the user does not make a choice.
|
||||
*
|
||||
* @return
|
||||
* `true` if the user clicks 'Yes',
|
||||
* `false` if the user clicks 'No' or cancels the dialog,
|
||||
* `undefined` if the dialog is closed without the user making a choice.
|
||||
*/
|
||||
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
|
||||
export async function showBinaryChoiceDialog(message: string, modal = true): Promise<boolean | undefined> {
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true };
|
||||
const chosenItem = await Window.showInformationMessage(message, { modal: true }, yesItem, noItem);
|
||||
const chosenItem = await Window.showInformationMessage(message, { modal }, yesItem, noItem);
|
||||
if (!chosenItem) {
|
||||
return undefined;
|
||||
}
|
||||
return chosenItem?.title === yesItem.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a modal dialog for the user to make a yes/no choice.
|
||||
*
|
||||
* @param message The message to show.
|
||||
* @param modal If true (the default), show a modal dialog box, otherwise dialog is non-modal and can
|
||||
* be closed even if the user does not make a choice.
|
||||
*
|
||||
* @return
|
||||
* `true` if the user clicks 'Yes',
|
||||
* `false` if the user clicks 'No' or cancels the dialog,
|
||||
* `undefined` if the dialog is closed without the user making a choice.
|
||||
*/
|
||||
export async function showBinaryChoiceWithUrlDialog(message: string, url: string): Promise<boolean | undefined> {
|
||||
const urlItem = { title: 'More Information', isCloseAffordance: false };
|
||||
const yesItem = { title: 'Yes', isCloseAffordance: false };
|
||||
const noItem = { title: 'No', isCloseAffordance: true };
|
||||
let chosenItem;
|
||||
|
||||
// Keep the dialog open as long as the user is clicking the 'more information' option.
|
||||
// To prevent an infinite loop, if the user clicks 'more information' 5 times, close the dialog and return cancelled
|
||||
let count = 0;
|
||||
do {
|
||||
chosenItem = await Window.showInformationMessage(message, { modal: true }, urlItem, yesItem, noItem);
|
||||
if (chosenItem === urlItem) {
|
||||
await env.openExternal(Uri.parse(url, true));
|
||||
}
|
||||
count++;
|
||||
} while (chosenItem === urlItem && count < 5);
|
||||
|
||||
if (!chosenItem || chosenItem.title === urlItem.title) {
|
||||
return undefined;
|
||||
}
|
||||
return chosenItem.title === yesItem.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an information message with a customisable action.
|
||||
* @param message The message to show.
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
RawResultsSortState,
|
||||
} from './pure/interface-types';
|
||||
import { Logger } from './logging';
|
||||
import { commandRunner } from './commandRunner';
|
||||
import * as messages from './pure/messages';
|
||||
import { commandRunner } from './commandRunner';
|
||||
import { CompletedQuery, interpretResults } from './query-results';
|
||||
import { QueryInfo, tmpDir } from './run-queries';
|
||||
import { parseSarifLocation, parseSarifPlainTextMessage } from './pure/sarif-utils';
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface QueryMetadata {
|
||||
description?: string;
|
||||
id?: string;
|
||||
kind?: string;
|
||||
scored?: string;
|
||||
}
|
||||
|
||||
export interface PreviousExecution {
|
||||
|
||||
@@ -413,6 +413,20 @@ export interface CompileUpgradeParams {
|
||||
singleFileUpgrades: true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for compiling an upgrade.
|
||||
*/
|
||||
export interface CompileUpgradeSequenceParams {
|
||||
/**
|
||||
* The sequence of upgrades to compile
|
||||
*/
|
||||
upgradePaths: string[];
|
||||
/**
|
||||
* A directory to store parts of the compiled upgrade
|
||||
*/
|
||||
upgradeTempDir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters describing an upgrade
|
||||
*/
|
||||
@@ -460,6 +474,19 @@ export interface CompileUpgradeResult {
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CompileUpgradeSequenceResult {
|
||||
/**
|
||||
* The compiled upgrades as a single file.
|
||||
*/
|
||||
compiledUpgrade?: string;
|
||||
/**
|
||||
* Any errors that occurred when checking the scripts.
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A description of a upgrade process
|
||||
*/
|
||||
@@ -498,7 +525,7 @@ export interface UpgradeDescription {
|
||||
}
|
||||
|
||||
|
||||
export type CompiledUpgrades = MultiFileCompiledUpgrades | SingleFileCompiledUpgrade
|
||||
export type CompiledUpgrades = MultiFileCompiledUpgrades | SingleFileCompiledUpgrades
|
||||
|
||||
/**
|
||||
* The parts shared by all compiled upgrades
|
||||
@@ -543,7 +570,7 @@ interface MultiFileCompiledUpgrades extends CompiledUpgradesBase {
|
||||
* A compiled upgrade.
|
||||
* The upgrade is in a single file.
|
||||
*/
|
||||
export interface SingleFileCompiledUpgrade extends CompiledUpgradesBase {
|
||||
export interface SingleFileCompiledUpgrades extends CompiledUpgradesBase {
|
||||
/**
|
||||
* The steps in the upgrade path
|
||||
*/
|
||||
@@ -696,6 +723,10 @@ export interface QueryToRun {
|
||||
* A uri pointing to the qlo to run.
|
||||
*/
|
||||
qlo: string;
|
||||
/**
|
||||
* A uri pointing to the compiled upgrade file.
|
||||
*/
|
||||
compiledUpgrade?: string;
|
||||
/**
|
||||
* The path where we should save this queries results
|
||||
*/
|
||||
@@ -972,7 +1003,10 @@ export const checkUpgrade = new rpc.RequestType<WithProgressId<UpgradeParams>, C
|
||||
* Compile an upgrade script to upgrade a dataset.
|
||||
*/
|
||||
export const compileUpgrade = new rpc.RequestType<WithProgressId<CompileUpgradeParams>, CompileUpgradeResult, void, void>('compilation/compileUpgrade');
|
||||
|
||||
/**
|
||||
* Compile an upgrade script to upgrade a dataset.
|
||||
*/
|
||||
export const compileUpgradeSequence = new rpc.RequestType<WithProgressId<CompileUpgradeSequenceParams>, CompileUpgradeSequenceResult, void, void>('compilation/compileUpgradeSequence');
|
||||
|
||||
/**
|
||||
* Clear the cache of a dataset
|
||||
|
||||
@@ -10,13 +10,11 @@ import {
|
||||
showAndLogWarningMessage,
|
||||
showBinaryChoiceDialog
|
||||
} from './helpers';
|
||||
import {
|
||||
commandRunner
|
||||
} from './commandRunner';
|
||||
import { logger } from './logging';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { QueryServerClient } from './queryserver-client';
|
||||
import { DisposableObject } from './vscode-utils/disposable-object';
|
||||
import { commandRunner } from './commandRunner';
|
||||
|
||||
/**
|
||||
* query-history.ts
|
||||
|
||||
@@ -173,7 +173,7 @@ export async function interpretResults(
|
||||
if (metadata === undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata');
|
||||
}
|
||||
let { kind, id } = metadata;
|
||||
let { kind, id, scored } = metadata;
|
||||
if (kind === undefined) {
|
||||
throw new Error('Can\'t interpret results without query metadata including kind');
|
||||
}
|
||||
@@ -182,5 +182,5 @@ export async function interpretResults(
|
||||
// SARIF format does, so in the absence of one, we use a dummy id.
|
||||
id = 'dummy-id';
|
||||
}
|
||||
return await server.interpretBqrs({ kind, id }, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
return await server.interpretBqrs({ kind, id, scored }, resultsPath, interpretedResultsPath, sourceInfo);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import * as cp from 'child_process';
|
||||
import * as path from 'path';
|
||||
import { DisposableObject } from './vscode-utils/disposable-object';
|
||||
import { Disposable } from 'vscode';
|
||||
import { CancellationToken, createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
|
||||
import { Disposable, CancellationToken, commands } from 'vscode';
|
||||
import { createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
|
||||
import * as cli from './cli';
|
||||
import { QueryServerConfig } from './config';
|
||||
import { Logger, ProgressReporter } from './logging';
|
||||
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './pure/messages';
|
||||
import * as messages from './pure/messages';
|
||||
import { SemVer } from 'semver';
|
||||
import { ProgressCallback, ProgressTask } from './commandRunner';
|
||||
|
||||
type ServerOpts = {
|
||||
logger: Logger;
|
||||
@@ -60,6 +61,16 @@ export class QueryServerClient extends DisposableObject {
|
||||
nextCallback: number;
|
||||
nextProgress: number;
|
||||
withProgressReporting: WithProgressReporting;
|
||||
|
||||
private readonly queryServerStartListeners = [] as ProgressTask<void>[];
|
||||
|
||||
// Can't use standard vscode EventEmitter here since they do not cause the calling
|
||||
// function to fail if one of the event handlers fail. This is something that
|
||||
// we need here.
|
||||
readonly onDidStartQueryServer = (e: ProgressTask<void>) => {
|
||||
this.queryServerStartListeners.push(e);
|
||||
}
|
||||
|
||||
public activeQueryName: string | undefined;
|
||||
|
||||
constructor(
|
||||
@@ -71,10 +82,8 @@ export class QueryServerClient extends DisposableObject {
|
||||
super();
|
||||
// When the query server configuration changes, restart the query server.
|
||||
if (config.onDidChangeConfiguration !== undefined) {
|
||||
this.push(config.onDidChangeConfiguration(async () => {
|
||||
this.logger.log('Restarting query server due to configuration changes...');
|
||||
await this.restartQueryServer();
|
||||
}, this));
|
||||
this.push(config.onDidChangeConfiguration(() =>
|
||||
commands.executeCommand('codeQL.restartQueryServer')));
|
||||
}
|
||||
this.withProgressReporting = withProgressReporting;
|
||||
this.nextCallback = 0;
|
||||
@@ -97,9 +106,19 @@ export class QueryServerClient extends DisposableObject {
|
||||
}
|
||||
|
||||
/** Restarts the query server by disposing of the current server process and then starting a new one. */
|
||||
async restartQueryServer(): Promise<void> {
|
||||
async restartQueryServer(
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
this.stopQueryServer();
|
||||
await this.startQueryServer();
|
||||
|
||||
// Ensure we await all responses from event handlers so that
|
||||
// errors can be properly reported to the user.
|
||||
await Promise.all(this.queryServerStartListeners.map(handler => handler(
|
||||
progress,
|
||||
token
|
||||
)));
|
||||
}
|
||||
|
||||
showLog(): void {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import * as tmp from 'tmp';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import {
|
||||
CancellationToken,
|
||||
ConfigurationTarget,
|
||||
@@ -14,7 +14,7 @@ import { ErrorCodes, ResponseError } from 'vscode-languageclient';
|
||||
|
||||
import * as cli from './cli';
|
||||
import * as config from './config';
|
||||
import { DatabaseItem, getUpgradesDirectories } from './databases';
|
||||
import { DatabaseItem } from './databases';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
|
||||
import { ProgressCallback, UserCancellationException } from './commandRunner';
|
||||
import { DatabaseInfo, QueryMetadata, ResultsPaths } from './pure/interface-types';
|
||||
@@ -23,7 +23,7 @@ import * as messages from './pure/messages';
|
||||
import { QueryHistoryItemOptions } from './query-history';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { isQuickQueryPath } from './quick-query';
|
||||
import { upgradeDatabase } from './upgrades';
|
||||
import { compileDatabaseUpgradeSequence, hasNondestructiveUpgradeCapabilities, upgradeDatabaseExplicit } from './upgrades';
|
||||
|
||||
/**
|
||||
* run-queries.ts
|
||||
@@ -80,6 +80,7 @@ export class QueryInfo {
|
||||
|
||||
async run(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeQlo: string | undefined,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<messages.EvaluationResult> {
|
||||
@@ -90,6 +91,7 @@ export class QueryInfo {
|
||||
const queryToRun: messages.QueryToRun = {
|
||||
resultsPath: this.resultsPaths.resultsPath,
|
||||
qlo: Uri.file(this.compiledQueryPath).toString(),
|
||||
compiledUpgrade: upgradeQlo && Uri.file(upgradeQlo).toString(),
|
||||
allowUnknownTemplates: true,
|
||||
templateValues: this.templates,
|
||||
id: callbackId,
|
||||
@@ -292,7 +294,7 @@ async function checkDbschemeCompatibility(
|
||||
const searchPath = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
|
||||
const { scripts, finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
|
||||
const { finalDbscheme } = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
|
||||
const hash = async function(filename: string): Promise<string> {
|
||||
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
|
||||
};
|
||||
@@ -311,18 +313,15 @@ async function checkDbschemeCompatibility(
|
||||
const upgradableTo = await hash(finalDbscheme);
|
||||
|
||||
if (upgradableTo != dbschemeOfLib) {
|
||||
logger.log(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but database has scheme ${query.program.dbschemePath}, and no upgrade path found`);
|
||||
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace. Please try using a newer version of the query libraries.`);
|
||||
reportNoUpgradePath(query);
|
||||
}
|
||||
|
||||
if (upgradableTo == dbschemeOfLib &&
|
||||
dbschemeOfDb != dbschemeOfLib) {
|
||||
// Try to upgrade the database
|
||||
await upgradeDatabase(
|
||||
await upgradeDatabaseExplicit(
|
||||
qs,
|
||||
query.dbItem,
|
||||
Uri.file(finalDbscheme),
|
||||
getUpgradesDirectories(scripts),
|
||||
progress,
|
||||
token
|
||||
);
|
||||
@@ -330,6 +329,42 @@ async function checkDbschemeCompatibility(
|
||||
}
|
||||
}
|
||||
|
||||
function reportNoUpgradePath(query: QueryInfo) {
|
||||
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace.\n\nPlease try using a newer version of the query libraries.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a non-destructive upgrade.
|
||||
*/
|
||||
async function compileNonDestructiveUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeTemp: tmp.DirectoryResult,
|
||||
query: QueryInfo,
|
||||
progress: ProgressCallback,
|
||||
token: CancellationToken,
|
||||
): Promise<string> {
|
||||
const searchPath = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (!query.dbItem?.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const { scripts, matchesTarget } = await qs.cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath, query.queryDbscheme);
|
||||
|
||||
if (!matchesTarget) {
|
||||
reportNoUpgradePath(query);
|
||||
}
|
||||
const result = await compileDatabaseUpgradeSequence(qs, query.dbItem, scripts, upgradeTemp, progress, token);
|
||||
if (result.compiledUpgrade === undefined) {
|
||||
const error = result.error || '[no error message available]';
|
||||
throw new Error(error);
|
||||
}
|
||||
// We can upgrade to the actual target
|
||||
query.program.dbschemePath = query.queryDbscheme;
|
||||
// We are new enough that we will always support single file upgrades.
|
||||
return result.compiledUpgrade;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to save `document` if it has unsaved changes.
|
||||
*
|
||||
@@ -516,67 +551,82 @@ export async function compileAndRunQueryAgainstDatabase(
|
||||
}
|
||||
|
||||
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata, templates);
|
||||
await checkDbschemeCompatibility(cliServer, qs, query, progress, token);
|
||||
|
||||
let errors;
|
||||
const upgradeDir = await tmp.dir({ dir: upgradesTmpDir.name, unsafeCleanup: true });
|
||||
try {
|
||||
errors = await query.compile(qs, progress, token);
|
||||
} catch (e) {
|
||||
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
|
||||
let upgradeQlo;
|
||||
if (await hasNondestructiveUpgradeCapabilities(qs)) {
|
||||
upgradeQlo = await compileNonDestructiveUpgrade(qs, upgradeDir, query, progress, token);
|
||||
} else {
|
||||
throw e;
|
||||
await checkDbschemeCompatibility(cliServer, qs, query, progress, token);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length == 0) {
|
||||
const result = await query.run(qs, progress, token);
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
const message = result.message || 'Failed to run query';
|
||||
logger.log(message);
|
||||
showAndLogErrorMessage(message);
|
||||
}
|
||||
return {
|
||||
query,
|
||||
result,
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
let errors;
|
||||
try {
|
||||
errors = await query.compile(qs, progress, token);
|
||||
} catch (e) {
|
||||
if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query cancelled', messages.QueryResultType.CANCELLATION);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
// so we include a general description of the problem,
|
||||
// and direct the user to the output window for the detailed compilation messages.
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
|
||||
|
||||
const formattedMessages: string[] = [];
|
||||
|
||||
for (const error of errors) {
|
||||
const message = error.message || '[no error message available]';
|
||||
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
|
||||
formattedMessages.push(formatted);
|
||||
qs.logger.log(formatted);
|
||||
}
|
||||
if (quickEval && formattedMessages.length <= 3) {
|
||||
showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
|
||||
|
||||
if (errors.length === 0) {
|
||||
const result = await query.run(qs, upgradeQlo, progress, token);
|
||||
if (result.resultType !== messages.QueryResultType.SUCCESS) {
|
||||
const message = result.message || 'Failed to run query';
|
||||
logger.log(message);
|
||||
showAndLogErrorMessage(message);
|
||||
}
|
||||
return {
|
||||
query,
|
||||
result,
|
||||
database: {
|
||||
name: db.name,
|
||||
databaseUri: db.databaseUri.toString(true)
|
||||
},
|
||||
options: historyItemOptions,
|
||||
logFileLocation: result.logFileLocation,
|
||||
dispose: () => {
|
||||
qs.logger.removeAdditionalLogLocation(result.logFileLocation);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') +
|
||||
' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
|
||||
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
|
||||
' and choose CodeQL Query Server from the dropdown.');
|
||||
}
|
||||
// Error dialogs are limited in size and scrollability,
|
||||
// so we include a general description of the problem,
|
||||
// and direct the user to the output window for the detailed compilation messages.
|
||||
// However we don't show quick eval errors there so we need to display them anyway.
|
||||
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
|
||||
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
const formattedMessages: string[] = [];
|
||||
|
||||
for (const error of errors) {
|
||||
const message = error.message || '[no error message available]';
|
||||
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
|
||||
formattedMessages.push(formatted);
|
||||
qs.logger.log(formatted);
|
||||
}
|
||||
if (quickEval && formattedMessages.length <= 3) {
|
||||
showAndLogErrorMessage('Quick evaluation compilation failed: \n' + formattedMessages.join('\n'));
|
||||
} else {
|
||||
showAndLogErrorMessage((quickEval ? 'Quick evaluation' : 'Query') + compilationFailedErrorTail);
|
||||
}
|
||||
|
||||
return createSyntheticResult(query, db, historyItemOptions, 'Query had compilation errors', messages.QueryResultType.OTHER_ERROR);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
upgradeDir.cleanup();
|
||||
} catch (e) {
|
||||
qs.logger.log(`Could not clean up the upgrades dir. Reason: ${e.message || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compilationFailedErrorTail = ' compilation failed. Please make sure there are no errors in the query, the database is up to date,' +
|
||||
' and the query and database use the same target language. For more details on the error, go to View > Output,' +
|
||||
' and choose CodeQL Query Server from the dropdown.';
|
||||
|
||||
function createSyntheticResult(
|
||||
query: QueryInfo,
|
||||
db: DatabaseItem,
|
||||
|
||||
42
extensions/ql-vscode/src/status-bar.ts
Normal file
42
extensions/ql-vscode/src/status-bar.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ConfigurationChangeEvent, StatusBarAlignment, StatusBarItem, window, workspace } from 'vscode';
|
||||
import { CodeQLCliServer } from './cli';
|
||||
import { CANARY_FEATURES, DistributionConfigListener } from './config';
|
||||
import { DisposableObject } from './vscode-utils/disposable-object';
|
||||
|
||||
/**
|
||||
* Creates and manages a status bar item for codeql. THis item contains
|
||||
* the current codeQL cli version as well as a notification if you are
|
||||
* in canary mode
|
||||
*
|
||||
*/
|
||||
export class CodeQlStatusBarHandler extends DisposableObject {
|
||||
|
||||
private readonly item: StatusBarItem;
|
||||
|
||||
constructor(private cli: CodeQLCliServer, distributionConfigListener: DistributionConfigListener) {
|
||||
super();
|
||||
this.item = window.createStatusBarItem(StatusBarAlignment.Right);
|
||||
this.push(this.item);
|
||||
this.push(workspace.onDidChangeConfiguration(this.handleDidChangeConfiguration, this));
|
||||
this.push(distributionConfigListener.onDidChangeConfiguration(() => this.updateStatusItem()));
|
||||
this.item.command = 'codeQL.openDocumentation';
|
||||
this.updateStatusItem();
|
||||
}
|
||||
|
||||
private handleDidChangeConfiguration(e: ConfigurationChangeEvent) {
|
||||
if (e.affectsConfiguration(CANARY_FEATURES.qualifiedName)) {
|
||||
this.updateStatusItem();
|
||||
}
|
||||
}
|
||||
|
||||
private async updateStatusItem() {
|
||||
const canary = CANARY_FEATURES.getValue() ? ' (Canary)' : '';
|
||||
// since getting the verison may take a few seconds, initialize with some
|
||||
// meaningful text.
|
||||
this.item.text = `CodeQL${canary}`;
|
||||
|
||||
const version = await this.cli.getVersion();
|
||||
this.item.text = `CodeQL CLI v${version}${canary}`;
|
||||
this.item.show();
|
||||
}
|
||||
}
|
||||
214
extensions/ql-vscode/src/telemetry.ts
Normal file
214
extensions/ql-vscode/src/telemetry.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { ConfigurationTarget, Extension, ExtensionContext, ConfigurationChangeEvent } from 'vscode';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import { ConfigListener, CANARY_FEATURES, ENABLE_TELEMETRY, GLOBAL_ENABLE_TELEMETRY, LOG_TELEMETRY } from './config';
|
||||
import * as appInsights from 'applicationinsights';
|
||||
import { logger } from './logging';
|
||||
import { UserCancellationException } from './commandRunner';
|
||||
import { showBinaryChoiceWithUrlDialog } from './helpers';
|
||||
|
||||
// Key is injected at build time through the APP_INSIGHTS_KEY environment variable.
|
||||
const key = 'REPLACE-APP-INSIGHTS-KEY';
|
||||
|
||||
export enum CommandCompletion {
|
||||
Success = 'Success',
|
||||
Failed = 'Failed',
|
||||
Cancelled = 'Cancelled'
|
||||
}
|
||||
|
||||
// Avoid sending the following data to App insights since we don't need it.
|
||||
const tagsToRemove = [
|
||||
'ai.application.ver',
|
||||
'ai.device.id',
|
||||
'ai.cloud.roleInstance',
|
||||
'ai.cloud.role',
|
||||
'ai.device.id',
|
||||
'ai.device.osArchitecture',
|
||||
'ai.device.osPlatform',
|
||||
'ai.device.osVersion',
|
||||
'ai.internal.sdkVersion',
|
||||
'ai.session.id'
|
||||
];
|
||||
|
||||
const baseDataPropertiesToRemove = [
|
||||
'common.os',
|
||||
'common.platformversion',
|
||||
'common.remotename',
|
||||
'common.uikind',
|
||||
'common.vscodesessionid'
|
||||
];
|
||||
|
||||
export class TelemetryListener extends ConfigListener {
|
||||
|
||||
static relevantSettings = [ENABLE_TELEMETRY, CANARY_FEATURES];
|
||||
|
||||
private reporter?: TelemetryReporter;
|
||||
|
||||
constructor(
|
||||
private readonly id: string,
|
||||
private readonly version: string,
|
||||
private readonly key: string,
|
||||
private readonly ctx: ExtensionContext
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handles changes to relevant configuration elements. There are 2 configuration
|
||||
* ids that this function cares about:
|
||||
*
|
||||
* * `codeQL.telemetry.enableTelemetry`: If this one has changed, then we need to re-initialize
|
||||
* the reporter and the reporter may wind up being removed.
|
||||
* * `codeQL.canary`: A change here could possibly re-trigger a dialog popup.
|
||||
*
|
||||
* Note that the global telemetry setting also gate-keeps whether or not to send telemetry events
|
||||
* to Application Insights. However, this gatekeeping happens inside of the vscode-extension-telemetry
|
||||
* package. So, this does not need to be handled here.
|
||||
*
|
||||
* @param e the configuration change event
|
||||
*/
|
||||
async handleDidChangeConfiguration(e: ConfigurationChangeEvent): Promise<void> {
|
||||
if (
|
||||
e.affectsConfiguration('codeQL.telemetry.enableTelemetry') ||
|
||||
e.affectsConfiguration('telemetry.enableTelemetry')
|
||||
) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// Re-request telemetry so that users can see the dialog again.
|
||||
// Re-request if codeQL.canary is being set to `true` and telemetry
|
||||
// is not currently enabled.
|
||||
if (
|
||||
e.affectsConfiguration('codeQL.canary') &&
|
||||
CANARY_FEATURES.getValue() &&
|
||||
!ENABLE_TELEMETRY.getValue()
|
||||
) {
|
||||
await Promise.all([
|
||||
this.setTelemetryRequested(false),
|
||||
this.requestTelemetryPermission()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.requestTelemetryPermission();
|
||||
|
||||
this.disposeReporter();
|
||||
|
||||
if (ENABLE_TELEMETRY.getValue<boolean>()) {
|
||||
this.createReporter();
|
||||
}
|
||||
}
|
||||
|
||||
private createReporter() {
|
||||
this.reporter = new TelemetryReporter(
|
||||
this.id,
|
||||
this.version,
|
||||
this.key,
|
||||
/* anonymize stack traces */ true
|
||||
);
|
||||
this.push(this.reporter);
|
||||
|
||||
const client = (this.reporter as any).appInsightsClient as appInsights.TelemetryClient;
|
||||
if (client) {
|
||||
// add a telemetry processor to delete unwanted properties
|
||||
client.addTelemetryProcessor((envelope: any) => {
|
||||
tagsToRemove.forEach(tag => delete envelope.tags[tag]);
|
||||
const baseDataProperties = (envelope.data as any)?.baseData?.properties;
|
||||
if (baseDataProperties) {
|
||||
baseDataPropertiesToRemove.forEach(prop => delete baseDataProperties[prop]);
|
||||
}
|
||||
|
||||
if (LOG_TELEMETRY.getValue<boolean>()) {
|
||||
logger.log(`Telemetry: ${JSON.stringify(envelope)}`);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this.reporter?.dispose();
|
||||
}
|
||||
|
||||
sendCommandUsage(name: string, executionTime: number, error?: Error) {
|
||||
if (!this.reporter) {
|
||||
return;
|
||||
}
|
||||
const status = !error
|
||||
? CommandCompletion.Success
|
||||
: error instanceof UserCancellationException
|
||||
? CommandCompletion.Cancelled
|
||||
: CommandCompletion.Failed;
|
||||
|
||||
const isCanary = (!!CANARY_FEATURES.getValue<boolean>()).toString();
|
||||
|
||||
this.reporter.sendTelemetryEvent(
|
||||
'command-usage',
|
||||
{
|
||||
name,
|
||||
status,
|
||||
isCanary
|
||||
},
|
||||
{ executionTime }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a popup asking the user if they want to enable telemetry
|
||||
* for this extension.
|
||||
*/
|
||||
async requestTelemetryPermission() {
|
||||
if (!this.wasTelemetryRequested()) {
|
||||
// if global telemetry is disabled, avoid showing the dialog or making any changes
|
||||
let result = undefined;
|
||||
if (GLOBAL_ENABLE_TELEMETRY.getValue()) {
|
||||
// Extension won't start until this completes.
|
||||
result = await showBinaryChoiceWithUrlDialog(
|
||||
'Does the CodeQL Extension by GitHub have your permission to collect usage data and metrics to help us improve CodeQL for VSCode?',
|
||||
'https://github.com/github/vscode-codeql/blob/main/extensions/ql-vscode/TELEMETRY.md'
|
||||
);
|
||||
}
|
||||
if (result !== undefined) {
|
||||
await Promise.all([
|
||||
this.setTelemetryRequested(true),
|
||||
ENABLE_TELEMETRY.updateValue<boolean>(result, ConfigurationTarget.Global),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposed for testing
|
||||
*/
|
||||
get _reporter() {
|
||||
return this.reporter;
|
||||
}
|
||||
|
||||
private disposeReporter() {
|
||||
if (this.reporter) {
|
||||
this.reporter.dispose();
|
||||
this.reporter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private wasTelemetryRequested(): boolean {
|
||||
return !!this.ctx.globalState.get<boolean>('telemetry-request-viewed');
|
||||
}
|
||||
|
||||
private async setTelemetryRequested(newValue: boolean): Promise<void> {
|
||||
await this.ctx.globalState.update('telemetry-request-viewed', newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The global Telemetry instance
|
||||
*/
|
||||
export let telemetryListener: TelemetryListener;
|
||||
|
||||
export async function initializeTelemetry(extension: Extension<any>, ctx: ExtensionContext): Promise<void> {
|
||||
telemetryListener = new TelemetryListener(extension.id, extension.packageJSON.version, key, ctx);
|
||||
telemetryListener.initialize();
|
||||
ctx.subscriptions.push(telemetryListener);
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { DatabaseItem } from './databases';
|
||||
import { showAndLogErrorMessage } from './helpers';
|
||||
import { getOnDiskWorkspaceFolders, showAndLogErrorMessage } from './helpers';
|
||||
import { ProgressCallback, UserCancellationException } from './commandRunner';
|
||||
import { logger } from './logging';
|
||||
import * as messages from './pure/messages';
|
||||
import * as qsClient from './queryserver-client';
|
||||
import { upgradesTmpDir } from './run-queries';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
import { DatabaseItem } from './databases';
|
||||
|
||||
/**
|
||||
* Maximum number of lines to include from database upgrade message,
|
||||
@@ -15,79 +18,97 @@ import { upgradesTmpDir } from './run-queries';
|
||||
const MAX_UPGRADE_MESSAGE_LINES = 10;
|
||||
|
||||
/**
|
||||
* Checks whether the given database can be upgraded to the given target DB scheme,
|
||||
* and whether the user wants to proceed with the upgrade.
|
||||
* Reports errors to both the user and the console.
|
||||
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
|
||||
* Check that we support non-destructive upgrades.
|
||||
*
|
||||
* This requires 3 features. The ability to compile an upgrade sequence; The ability to
|
||||
* run a non-destructive upgrades as a query; the ability to specify a target when
|
||||
* resolving upgrades. We check for a version of codeql that has all three features.
|
||||
*/
|
||||
async function checkAndConfirmDatabaseUpgrade(
|
||||
export async function hasNondestructiveUpgradeCapabilities(qs: qsClient.QueryServerClient): Promise<boolean> {
|
||||
return semver.gte(await qs.cliServer.getVersion(), '2.4.2');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compile a database upgrade sequence.
|
||||
* Callers must check that this is valid with the current queryserver first.
|
||||
*/
|
||||
export async function compileDatabaseUpgradeSequence(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
targetDbScheme: vscode.Uri,
|
||||
upgradesDirectories: vscode.Uri[],
|
||||
resolvedSequence: string[],
|
||||
currentUpgradeTmp: tmp.DirectoryResult,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<messages.UpgradeParams | undefined> {
|
||||
token: vscode.CancellationToken
|
||||
): Promise<messages.CompileUpgradeSequenceResult> {
|
||||
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const params: messages.UpgradeParams = {
|
||||
fromDbscheme: db.contents.dbSchemeUri.fsPath,
|
||||
toDbscheme: targetDbScheme.fsPath,
|
||||
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
|
||||
};
|
||||
if (!await hasNondestructiveUpgradeCapabilities(qs)) {
|
||||
throw new Error('The version of codeql is too old to run non-destructive upgrades.');
|
||||
}
|
||||
// If possible just compile the upgrade sequence
|
||||
return await qs.sendRequest(messages.compileUpgradeSequence, {
|
||||
upgradeTempDir: currentUpgradeTmp.path,
|
||||
upgradePaths: resolvedSequence
|
||||
}, token, progress);
|
||||
}
|
||||
|
||||
let checkUpgradeResult: messages.CheckUpgradeResult;
|
||||
try {
|
||||
qs.logger.log('Checking database upgrade...');
|
||||
checkUpgradeResult = await checkDatabaseUpgrade(qs, params, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Database cannot be upgraded: ${e}`);
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done checking database upgrade.');
|
||||
async function compileDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem,
|
||||
targetDbScheme: string,
|
||||
resolvedSequence: string[],
|
||||
currentUpgradeTmp: tmp.DirectoryResult,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<messages.CompileUpgradeResult> {
|
||||
if (!db.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
// We have the upgrades we want but compileUpgrade
|
||||
// requires searching for them. So we use the parent directories of the upgrades
|
||||
// as the upgrade path.
|
||||
const parentDirs = resolvedSequence.map(dir => path.dirname(dir));
|
||||
const uniqueParentDirs = new Set(parentDirs);
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 3,
|
||||
message: 'Checking for database upgrades'
|
||||
});
|
||||
return qs.sendRequest(messages.compileUpgrade, {
|
||||
upgrade: {
|
||||
fromDbscheme: db.contents.dbSchemeUri.fsPath,
|
||||
toDbscheme: targetDbScheme,
|
||||
additionalUpgrades: Array.from(uniqueParentDirs)
|
||||
},
|
||||
upgradeTempDir: currentUpgradeTmp.path,
|
||||
singleFileUpgrades: true,
|
||||
}, token, progress);
|
||||
}
|
||||
|
||||
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
|
||||
if (checkedUpgrades === undefined) {
|
||||
const error = checkUpgradeResult.upgradeError || '[no error message available]';
|
||||
throw new Error(`Database cannot be upgraded: ${error}`);
|
||||
}
|
||||
/**
|
||||
* Checks whether the user wants to proceed with the upgrade.
|
||||
* Reports errors to both the user and the console.
|
||||
*/
|
||||
async function checkAndConfirmDatabaseUpgrade(
|
||||
compiled: messages.CompiledUpgrades,
|
||||
db: DatabaseItem,
|
||||
quiet: boolean
|
||||
): Promise<void> {
|
||||
|
||||
if (checkedUpgrades.scripts.length === 0) {
|
||||
progress({
|
||||
step: 3,
|
||||
maxStep: 3,
|
||||
message: 'Database is already up to date; nothing to do.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let curSha = checkedUpgrades.initialSha;
|
||||
let descriptionMessage = '';
|
||||
for (const script of checkedUpgrades.scripts) {
|
||||
const descriptions = getUpgradeDescriptions(compiled);
|
||||
for (const script of descriptions) {
|
||||
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
|
||||
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
|
||||
curSha = script.newSha;
|
||||
}
|
||||
|
||||
const targetSha = checkedUpgrades.targetSha;
|
||||
if (curSha != targetSha) {
|
||||
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
|
||||
// A modal dialog would be rendered better, but is more intrusive.
|
||||
await showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
|
||||
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
|
||||
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(descriptionMessage);
|
||||
|
||||
|
||||
// If the quiet flag is set, do the upgrade without a popup.
|
||||
if (qs.cliServer.quiet) {
|
||||
return params;
|
||||
if (quiet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask the user to confirm the upgrade.
|
||||
@@ -111,106 +132,90 @@ async function checkAndConfirmDatabaseUpgrade(
|
||||
logger.outputChannel.show();
|
||||
}
|
||||
|
||||
if (chosenItem === yesItem) {
|
||||
return params;
|
||||
}
|
||||
else {
|
||||
if (chosenItem !== yesItem) {
|
||||
throw new UserCancellationException('User cancelled the database upgrade.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the descriptions from a compiled upgrade
|
||||
*/
|
||||
function getUpgradeDescriptions(compiled: messages.CompiledUpgrades): messages.UpgradeDescription[] {
|
||||
// We use the presence of compiledUpgradeFile to check
|
||||
// if it is multifile or not. We need to explicitly check undefined
|
||||
// as the types claim the empty string is a valid value
|
||||
if (compiled.compiledUpgradeFile === undefined) {
|
||||
return compiled.scripts.map(script => script.description);
|
||||
} else {
|
||||
return compiled.descriptions;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for 'Upgrade Database'.
|
||||
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
|
||||
* First performs a dry-run and prompts the user to confirm the upgrade.
|
||||
* Reports errors during compilation and evaluation of upgrades to the user.
|
||||
*/
|
||||
export async function upgradeDatabase(
|
||||
export async function upgradeDatabaseExplicit(
|
||||
qs: qsClient.QueryServerClient,
|
||||
db: DatabaseItem, targetDbScheme: vscode.Uri,
|
||||
upgradesDirectories: vscode.Uri[],
|
||||
db: DatabaseItem,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<messages.RunUpgradeResult | undefined> {
|
||||
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories, progress, token);
|
||||
|
||||
if (upgradeParams === undefined) {
|
||||
return;
|
||||
const searchPath: string[] = getOnDiskWorkspaceFolders();
|
||||
|
||||
if (!db?.contents?.dbSchemeUri) {
|
||||
throw new Error('Database is invalid, and cannot be upgraded.');
|
||||
}
|
||||
const upgradeInfo = await qs.cliServer.resolveUpgrades(
|
||||
db.contents.dbSchemeUri.fsPath,
|
||||
searchPath
|
||||
);
|
||||
|
||||
let compileUpgradeResult: messages.CompileUpgradeResult;
|
||||
const { scripts, finalDbscheme } = upgradeInfo;
|
||||
|
||||
if (finalDbscheme === undefined) {
|
||||
throw new Error('Could not determine target dbscheme to upgrade to.');
|
||||
}
|
||||
const currentUpgradeTmp = await tmp.dir({ dir: upgradesTmpDir.name, prefix: 'upgrade_', keep: false, unsafeCleanup: true });
|
||||
try {
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done compiling database upgrade.');
|
||||
}
|
||||
|
||||
if (compileUpgradeResult.compiledUpgrades === undefined) {
|
||||
const error = compileUpgradeResult.error || '[no error message available]';
|
||||
(`Compilation of database upgrades failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
qs.logger.log('Running the following database upgrade:');
|
||||
// We use the presence of compiledUpgradeFile to check
|
||||
// if it is multifile or not. We need to explicitly check undefined
|
||||
// as the types claim the empty string is a valid value
|
||||
if (compileUpgradeResult.compiledUpgrades.compiledUpgradeFile === undefined) {
|
||||
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
|
||||
} else {
|
||||
qs.logger.log(compileUpgradeResult.compiledUpgrades.descriptions.map(s => s.description).join('\n'));
|
||||
let compileUpgradeResult: messages.CompileUpgradeResult;
|
||||
try {
|
||||
compileUpgradeResult = await compileDatabaseUpgrade(qs, db, finalDbscheme, scripts, currentUpgradeTmp, progress, token);
|
||||
}
|
||||
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
catch (e) {
|
||||
showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done compiling database upgrade.');
|
||||
}
|
||||
|
||||
if (!compileUpgradeResult.compiledUpgrades) {
|
||||
const error = compileUpgradeResult.error || '[no error message available]';
|
||||
showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await checkAndConfirmDatabaseUpgrade(compileUpgradeResult.compiledUpgrades, db, qs.cliServer.quiet);
|
||||
|
||||
try {
|
||||
qs.logger.log('Running the following database upgrade:');
|
||||
|
||||
getUpgradeDescriptions(compileUpgradeResult.compiledUpgrades).map(s => s.description).join('\n');
|
||||
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades, progress, token);
|
||||
}
|
||||
catch (e) {
|
||||
showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
return;
|
||||
} finally {
|
||||
qs.logger.log('Done running database upgrade.');
|
||||
}
|
||||
} finally {
|
||||
currentUpgradeTmp.cleanup();
|
||||
}
|
||||
catch (e) {
|
||||
showAndLogErrorMessage(`Database upgrade failed: ${e}`);
|
||||
return;
|
||||
}
|
||||
finally {
|
||||
qs.logger.log('Done running database upgrade.');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeParams: messages.UpgradeParams,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<messages.CheckUpgradeResult> {
|
||||
progress({
|
||||
step: 1,
|
||||
maxStep: 3,
|
||||
message: 'Checking for database upgrades'
|
||||
});
|
||||
|
||||
return qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress);
|
||||
}
|
||||
|
||||
async function compileDatabaseUpgrade(
|
||||
qs: qsClient.QueryServerClient,
|
||||
upgradeParams: messages.UpgradeParams,
|
||||
progress: ProgressCallback,
|
||||
token: vscode.CancellationToken,
|
||||
): Promise<messages.CompileUpgradeResult> {
|
||||
const params: messages.CompileUpgradeParams = {
|
||||
upgrade: upgradeParams,
|
||||
upgradeTempDir: upgradesTmpDir.name,
|
||||
singleFileUpgrades: true
|
||||
};
|
||||
|
||||
progress({
|
||||
step: 2,
|
||||
maxStep: 3,
|
||||
message: 'Compiling database upgrades'
|
||||
});
|
||||
|
||||
return qs.sendRequest(messages.compileUpgrade, params, token, progress);
|
||||
}
|
||||
|
||||
async function runDatabaseUpgrade(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fail } from 'assert';
|
||||
import { CancellationToken, extensions, Uri } from 'vscode';
|
||||
import { CancellationToken, commands, extensions, Uri } from 'vscode';
|
||||
import * as sinon from 'sinon';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
@@ -14,6 +14,7 @@ import { compileAndRunQueryAgainstDatabase } from '../../run-queries';
|
||||
import { CodeQLCliServer } from '../../cli';
|
||||
import { QueryServerClient } from '../../queryserver-client';
|
||||
import { skipIfNoCodeQL } from '../ensureCli';
|
||||
import { QueryResultType } from '../../pure/messages';
|
||||
|
||||
|
||||
/**
|
||||
@@ -94,10 +95,35 @@ describe('Queries', function() {
|
||||
// just check that the query was successful
|
||||
expect(result.database.name).to.eq('db');
|
||||
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
|
||||
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
|
||||
} catch (e) {
|
||||
console.error('Test Failed');
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Asserts a fix for bug https://github.com/github/vscode-codeql/issues/733
|
||||
it('should restart the database and run a query', async () => {
|
||||
try {
|
||||
await commands.executeCommand('codeQL.restartQueryServer');
|
||||
const queryPath = path.join(__dirname, 'data', 'simple-query.ql');
|
||||
const result = await compileAndRunQueryAgainstDatabase(
|
||||
cli,
|
||||
qs,
|
||||
dbItem,
|
||||
false,
|
||||
Uri.file(queryPath),
|
||||
progress,
|
||||
token
|
||||
);
|
||||
|
||||
// this message would indicate that the databases were not properly reregistered
|
||||
expect(result.result.message).not.to.eq('No result from server');
|
||||
expect(result.options.queryText).to.eq(fs.readFileSync(queryPath, 'utf8'));
|
||||
expect(result.result.resultType).to.eq(QueryResultType.SUCCESS);
|
||||
} catch (e) {
|
||||
console.error('Test Failed');
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,10 @@ class Checkpoint<T> {
|
||||
constructor() {
|
||||
this.res = () => { /**/ };
|
||||
this.rej = () => { /**/ };
|
||||
this.promise = new Promise((res, rej) => { this.res = res; this.rej = rej; });
|
||||
this.promise = new Promise((res, rej) => {
|
||||
this.res = res as () => {};
|
||||
this.rej = rej;
|
||||
});
|
||||
}
|
||||
|
||||
async done(): Promise<T> {
|
||||
@@ -81,6 +84,11 @@ const queryTestCases: QueryTestCase[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const db: messages.Dataset = {
|
||||
dbDir: path.join(__dirname, '../test-db'),
|
||||
workingSet: 'default',
|
||||
};
|
||||
|
||||
describe('using the query server', function() {
|
||||
before(function() {
|
||||
skipIfNoCodeQL(this);
|
||||
@@ -120,6 +128,12 @@ describe('using the query server', function() {
|
||||
const evaluationSucceeded = new Checkpoint<void>();
|
||||
const parsedResults = new Checkpoint<void>();
|
||||
|
||||
it('should register the database if necessary', async () => {
|
||||
if (await qs.supportsDatabaseRegistration()) {
|
||||
await qs.sendRequest(messages.registerDatabases, { databases: [db] }, token, (() => { /**/ }) as any);
|
||||
}
|
||||
});
|
||||
|
||||
it(`should be able to compile query ${queryName}`, async function() {
|
||||
await queryServerStarted.done();
|
||||
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
|
||||
@@ -166,15 +180,11 @@ describe('using the query server', function() {
|
||||
id: callbackId,
|
||||
timeoutSecs: 1000,
|
||||
};
|
||||
const db: messages.Dataset = {
|
||||
dbDir: path.join(__dirname, '../test-db'),
|
||||
workingSet: 'default',
|
||||
};
|
||||
const params: messages.EvaluateQueriesParams = {
|
||||
db,
|
||||
evaluateId: callbackId,
|
||||
queries: [queryToRun],
|
||||
stopOnError: false,
|
||||
stopOnError: true,
|
||||
useSequenceHint: false
|
||||
};
|
||||
await qs.sendRequest(messages.runQueries, params, token, () => { /**/ });
|
||||
|
||||
@@ -28,7 +28,10 @@ import { workspace } from 'vscode';
|
||||
process.on('unhandledRejection', e => {
|
||||
console.error('Unhandled rejection.');
|
||||
console.error(e);
|
||||
process.exit(-1);
|
||||
// Must use a setTimeout in order to ensure the log is fully flushed before exiting
|
||||
setTimeout(() => {
|
||||
process.exit(-1);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
const _1MB = 1024 * 1024;
|
||||
@@ -36,7 +39,7 @@ const _10MB = _1MB * 10;
|
||||
|
||||
// CLI version to test. Hard code the latest as default. And be sure
|
||||
// to update the env if it is not otherwise set.
|
||||
const CLI_VERSION = process.env.CLI_VERSION || 'v2.4.0';
|
||||
const CLI_VERSION = process.env.CLI_VERSION || 'v2.4.2';
|
||||
process.env.CLI_VERSION = CLI_VERSION;
|
||||
|
||||
// Base dir where CLIs will be downloaded into
|
||||
|
||||
@@ -66,7 +66,8 @@ describe('databases', () => {
|
||||
} as unknown as ExtensionContext,
|
||||
{
|
||||
sendRequest: sendRequestSpy,
|
||||
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy
|
||||
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy,
|
||||
onDidStartQueryServer: () => { /**/ }
|
||||
} as unknown as QueryServerClient,
|
||||
{
|
||||
supportsLanguageName: supportsLanguageNameSpy,
|
||||
|
||||
@@ -21,17 +21,19 @@ describe('databaseFetcher', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
describe('convertToDatabaseUrl', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
let quickPickSpy: sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
quickPickSpy = sinon.stub(window, 'showQuickPick');
|
||||
sandbox = sinon.createSandbox();
|
||||
quickPickSpy = sandbox.stub(window, 'showQuickPick');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(window.showQuickPick as sinon.SinonStub).restore();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('should convert a project url to a database url', async () => {
|
||||
quickPickSpy.returns('javascript' as any);
|
||||
quickPickSpy.resolves('javascript');
|
||||
const lgtmUrl = 'https://lgtm.com/projects/g/github/codeql';
|
||||
const dbUrl = await convertToDatabaseUrl(lgtmUrl);
|
||||
|
||||
@@ -43,7 +45,7 @@ describe('databaseFetcher', function() {
|
||||
});
|
||||
|
||||
it('should convert a project url to a database url with extra path segments', async () => {
|
||||
quickPickSpy.returns('python' as any);
|
||||
quickPickSpy.resolves('python');
|
||||
const lgtmUrl =
|
||||
'https://lgtm.com/projects/g/github/codeql/subpage/subpage2?query=xxx';
|
||||
const dbUrl = await convertToDatabaseUrl(lgtmUrl);
|
||||
@@ -54,7 +56,7 @@ describe('databaseFetcher', function() {
|
||||
});
|
||||
|
||||
it('should fail on a nonexistant prohect', async () => {
|
||||
quickPickSpy.returns('javascript' as any);
|
||||
quickPickSpy.resolves('javascript');
|
||||
const lgtmUrl = 'https://lgtm.com/projects/g/github/hucairz';
|
||||
expect(convertToDatabaseUrl(lgtmUrl)).to.rejectedWith(/Invalid LGTM URL/);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,6 @@ describe('databases-ui', () => {
|
||||
const db5 = createDatabase(storageDir, 'db2-notimported-with-codeql-database.yml', 'cpp', 'codeql-database.yml');
|
||||
|
||||
const databaseUI = new DatabaseUI(
|
||||
{} as any,
|
||||
{
|
||||
databaseItems: [
|
||||
{ databaseUri: Uri.file(db1) }
|
||||
|
||||
@@ -190,10 +190,11 @@ describe('Launcher path', () => {
|
||||
let launcherThatExists = '';
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox();
|
||||
getExecutableFromDirectory = createModule().getExecutableFromDirectory;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
@@ -300,7 +301,6 @@ describe('Launcher path', () => {
|
||||
});
|
||||
|
||||
function createModule() {
|
||||
sandbox = sinon.createSandbox();
|
||||
warnSpy = sandbox.spy();
|
||||
errorSpy = sandbox.spy();
|
||||
logSpy = sandbox.spy();
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { expect } from 'chai';
|
||||
import 'mocha';
|
||||
import { ExtensionContext, Memento } from 'vscode';
|
||||
import { ExtensionContext, Memento, window } from 'vscode';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as tmp from 'tmp';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder } from '../../helpers';
|
||||
import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder, showBinaryChoiceDialog, showBinaryChoiceWithUrlDialog, showInformationMessageWithAction } from '../../helpers';
|
||||
import { reportStreamProgress } from '../../commandRunner';
|
||||
import Sinon = require('sinon');
|
||||
import { fail } from 'assert';
|
||||
|
||||
describe('helpers', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -225,4 +227,91 @@ describe('helpers', () => {
|
||||
message: 'My prefix (Size unknown)',
|
||||
});
|
||||
});
|
||||
|
||||
describe('open dialog', () => {
|
||||
let showInformationMessageSpy: Sinon.SinonStub;
|
||||
beforeEach(() => {
|
||||
showInformationMessageSpy = sandbox.stub(window, 'showInformationMessage');
|
||||
});
|
||||
|
||||
it('should show a binary choice dialog and return `yes`', (done) => {
|
||||
// pretend user chooses 'yes'
|
||||
showInformationMessageSpy.onCall(0).resolvesArg(2);
|
||||
const res = showBinaryChoiceDialog('xxx');
|
||||
res.then((val) => {
|
||||
expect(val).to.eq(true);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
|
||||
it('should show a binary choice dialog and return `no`', (done) => {
|
||||
// pretend user chooses 'no'
|
||||
showInformationMessageSpy.onCall(0).resolvesArg(3);
|
||||
const res = showBinaryChoiceDialog('xxx');
|
||||
res.then((val) => {
|
||||
expect(val).to.eq(false);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
|
||||
it('should show an info dialog and confirm the action', (done) => {
|
||||
// pretend user chooses to run action
|
||||
showInformationMessageSpy.onCall(0).resolvesArg(1);
|
||||
const res = showInformationMessageWithAction('xxx', 'yyy');
|
||||
res.then((val) => {
|
||||
expect(val).to.eq(true);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
|
||||
it('should show an action dialog and avoid choosing the action', (done) => {
|
||||
// pretend user does not choose to run action
|
||||
showInformationMessageSpy.onCall(0).resolves(undefined);
|
||||
const res = showInformationMessageWithAction('xxx', 'yyy');
|
||||
res.then((val) => {
|
||||
expect(val).to.eq(false);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
|
||||
it('should show a binary choice dialog with a url and return `yes`', (done) => {
|
||||
// pretend user clicks on the url twice and then clicks 'yes'
|
||||
showInformationMessageSpy.onCall(0).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(1).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(2).resolvesArg(3);
|
||||
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
|
||||
res.then((val) => {
|
||||
expect(val).to.eq(true);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
|
||||
it('should show a binary choice dialog with a url and return `no`', (done) => {
|
||||
// pretend user clicks on the url twice and then clicks 'no'
|
||||
showInformationMessageSpy.onCall(0).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(1).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(2).resolvesArg(4);
|
||||
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
|
||||
res.then((val) => {
|
||||
expect(val).to.eq(false);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
|
||||
it('should show a binary choice dialog and exit after clcking `more info` 5 times', (done) => {
|
||||
// pretend user clicks on the url twice and then clicks 'no'
|
||||
showInformationMessageSpy.onCall(0).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(1).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(2).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(3).resolvesArg(2);
|
||||
showInformationMessageSpy.onCall(4).resolvesArg(2);
|
||||
const res = showBinaryChoiceWithUrlDialog('xxx', 'invalid:url');
|
||||
res.then((val) => {
|
||||
// No choie was made
|
||||
expect(val).to.eq(undefined);
|
||||
expect(showInformationMessageSpy.getCalls().length).to.eq(5);
|
||||
done();
|
||||
}).catch(e => fail(e));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +152,8 @@ describe('CompletedQuery', () => {
|
||||
const sourceInfo = {};
|
||||
const metadata = {
|
||||
kind: 'my-kind',
|
||||
id: 'my-id' as string | undefined
|
||||
id: 'my-id' as string | undefined,
|
||||
scored: undefined
|
||||
};
|
||||
const results1 = await interpretResults(
|
||||
mockServer,
|
||||
@@ -183,7 +184,7 @@ describe('CompletedQuery', () => {
|
||||
);
|
||||
expect(results2).to.eq('1234');
|
||||
expect(spy).to.have.been.calledWith(
|
||||
{ kind: 'my-kind', id: 'dummy-id' },
|
||||
{ kind: 'my-kind', id: 'dummy-id', scored: undefined },
|
||||
resultsPath, interpretedResultsPath, sourceInfo
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
import 'sinon-chai';
|
||||
import * as sinon from 'sinon';
|
||||
import * as chaiAsPromised from 'chai-as-promised';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import { ExtensionContext, workspace, ConfigurationTarget, window } from 'vscode';
|
||||
import { TelemetryListener, telemetryListener as globalTelemetryListener } from '../../telemetry';
|
||||
import { UserCancellationException } from '../../commandRunner';
|
||||
import { fail } from 'assert';
|
||||
import { ENABLE_TELEMETRY } from '../../config';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
const expect = chai.expect;
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
describe('telemetry reporting', function() {
|
||||
// setting preferences can trigger lots of background activity
|
||||
// so need to bump up the timeout of this test.
|
||||
this.timeout(10000);
|
||||
|
||||
let originalTelemetryExtension: boolean | undefined;
|
||||
let originalTelemetryGlobal: boolean | undefined;
|
||||
let isCanary: string;
|
||||
let ctx: ExtensionContext;
|
||||
let telemetryListener: TelemetryListener;
|
||||
|
||||
beforeEach(async () => {
|
||||
try {
|
||||
// in case a previous test has accidentally activated this extension,
|
||||
// need to disable it first.
|
||||
// Accidentaly activation may happen asynchronously due to activationEvents
|
||||
// specified in the package.json.
|
||||
globalTelemetryListener?.dispose();
|
||||
|
||||
ctx = createMockExtensionContext();
|
||||
|
||||
sandbox.stub(TelemetryReporter.prototype, 'sendTelemetryEvent');
|
||||
sandbox.stub(TelemetryReporter.prototype, 'sendTelemetryException');
|
||||
sandbox.stub(TelemetryReporter.prototype, 'dispose');
|
||||
|
||||
originalTelemetryExtension = workspace.getConfiguration().get<boolean>('codeQL.telemetry.enableTelemetry');
|
||||
originalTelemetryGlobal = workspace.getConfiguration().get<boolean>('telemetry.enableTelemetry');
|
||||
isCanary = (!!workspace.getConfiguration().get<boolean>('codeQL.canary')).toString();
|
||||
|
||||
// each test will default to telemetry being enabled
|
||||
await enableTelemetry('telemetry', true);
|
||||
await enableTelemetry('codeQL.telemetry', true);
|
||||
|
||||
telemetryListener = new TelemetryListener('my-id', '1.2.3', 'fake-key', ctx);
|
||||
await wait(100);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
telemetryListener?.dispose();
|
||||
// await wait(100);
|
||||
try {
|
||||
sandbox.restore();
|
||||
await enableTelemetry('telemetry', originalTelemetryGlobal);
|
||||
await enableTelemetry('codeQL.telemetry', originalTelemetryExtension);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize telemetry when both options are enabled', async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
expect(telemetryListener._reporter).not.to.be.undefined;
|
||||
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.extensionId).to.eq('my-id');
|
||||
expect(reporter.extensionVersion).to.eq('1.2.3');
|
||||
expect(reporter.userOptIn).to.eq(true); // enabled
|
||||
});
|
||||
|
||||
it('should initialize telemetry when global option disabled', async () => {
|
||||
try {
|
||||
await enableTelemetry('telemetry', false);
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).not.to.be.undefined;
|
||||
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.userOptIn).to.eq(false); // disabled
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not initialize telemetry when extension option disabled', async () => {
|
||||
try {
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
await telemetryListener.initialize();
|
||||
|
||||
expect(telemetryListener._reporter).to.be.undefined;
|
||||
} catch (e) {
|
||||
fail(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not initialize telemetry when both options disabled', async () => {
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
await enableTelemetry('telemetry', false);
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should dispose telemetry object when re-initializing and should not add multiple', async () => {
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).not.to.be.undefined;
|
||||
const firstReporter = telemetryListener._reporter;
|
||||
await telemetryListener.initialize();
|
||||
expect(telemetryListener._reporter).not.to.be.undefined;
|
||||
expect(telemetryListener._reporter).not.to.eq(firstReporter);
|
||||
|
||||
expect(TelemetryReporter.prototype.dispose).to.have.been.calledOnce;
|
||||
|
||||
// initializing a third time continues to dispose
|
||||
await telemetryListener.initialize();
|
||||
expect(TelemetryReporter.prototype.dispose).to.have.been.calledTwice;
|
||||
});
|
||||
|
||||
it('should reinitialize reporter when extension setting changes', async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
expect(TelemetryReporter.prototype.dispose).not.to.have.been.called;
|
||||
expect(telemetryListener._reporter).not.to.be.undefined;
|
||||
|
||||
// this disables the reporter
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
|
||||
expect(telemetryListener._reporter).to.be.undefined;
|
||||
|
||||
expect(TelemetryReporter.prototype.dispose).to.have.been.calledOnce;
|
||||
|
||||
// creates a new reporter, but does not dispose again
|
||||
await enableTelemetry('codeQL.telemetry', true);
|
||||
|
||||
expect(telemetryListener._reporter).not.to.be.undefined;
|
||||
expect(TelemetryReporter.prototype.dispose).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should set userOprIn to false when global setting changes', async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
const reporter: any = telemetryListener._reporter;
|
||||
expect(reporter.userOptIn).to.eq(true); // enabled
|
||||
|
||||
await enableTelemetry('telemetry', false);
|
||||
expect(reporter.userOptIn).to.eq(false); // disabled
|
||||
});
|
||||
|
||||
it('should send an event', async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendCommandUsage('command-id', 1234, undefined);
|
||||
|
||||
expect(TelemetryReporter.prototype.sendTelemetryEvent).to.have.been.calledOnceWith('command-usage',
|
||||
{
|
||||
name: 'command-id',
|
||||
status: 'Success',
|
||||
isCanary
|
||||
},
|
||||
{ executionTime: 1234 });
|
||||
|
||||
expect(TelemetryReporter.prototype.sendTelemetryException).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should send a command usage event with an error', async () => {
|
||||
await telemetryListener.initialize();
|
||||
|
||||
telemetryListener.sendCommandUsage('command-id', 1234, new UserCancellationException());
|
||||
|
||||
expect(TelemetryReporter.prototype.sendTelemetryEvent).to.have.been.calledOnceWith('command-usage',
|
||||
{
|
||||
name: 'command-id',
|
||||
status: 'Cancelled',
|
||||
isCanary
|
||||
},
|
||||
{ executionTime: 1234 });
|
||||
|
||||
expect(TelemetryReporter.prototype.sendTelemetryException).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should avoid sending an event when telemetry is disabled', async () => {
|
||||
await telemetryListener.initialize();
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
|
||||
telemetryListener.sendCommandUsage('command-id', 1234, undefined);
|
||||
telemetryListener.sendCommandUsage('command-id', 1234, new Error());
|
||||
|
||||
expect(TelemetryReporter.prototype.sendTelemetryEvent).not.to.have.been.called;
|
||||
expect(TelemetryReporter.prototype.sendTelemetryException).not.to.have.been.called;
|
||||
});
|
||||
|
||||
it('should send an event when telemetry is re-enabled', async () => {
|
||||
await telemetryListener.initialize();
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
await enableTelemetry('codeQL.telemetry', true);
|
||||
|
||||
telemetryListener.sendCommandUsage('command-id', 1234, undefined);
|
||||
|
||||
expect(TelemetryReporter.prototype.sendTelemetryEvent).to.have.been.calledOnceWith('command-usage',
|
||||
{
|
||||
name: 'command-id',
|
||||
status: 'Success',
|
||||
isCanary
|
||||
},
|
||||
{ executionTime: 1234 });
|
||||
});
|
||||
|
||||
it('should filter undesired properties from telemetry payload', async () => {
|
||||
await telemetryListener.initialize();
|
||||
// Reach into the internal appInsights client to grab our telemetry processor.
|
||||
const telemetryProcessor: Function =
|
||||
((telemetryListener._reporter as any).appInsightsClient._telemetryProcessors)[0];
|
||||
const envelop = {
|
||||
tags: {
|
||||
'ai.cloud.roleInstance': true,
|
||||
other: true
|
||||
},
|
||||
data: {
|
||||
baseData: {
|
||||
properties: {
|
||||
'common.remotename': true,
|
||||
other: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const res = telemetryProcessor(envelop);
|
||||
expect(res).to.eq(true);
|
||||
expect(envelop).to.deep.eq({
|
||||
tags: {
|
||||
other: true
|
||||
},
|
||||
data: {
|
||||
baseData: {
|
||||
properties: {
|
||||
other: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should request permission if popup has never been seen before', async () => {
|
||||
sandbox.stub(window, 'showInformationMessage').resolvesArg(3 /* "yes" item */);
|
||||
await ctx.globalState.update('telemetry-request-viewed', false);
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
|
||||
await telemetryListener.initialize();
|
||||
|
||||
// Dialog opened, user clicks "yes" and telemetry enabled
|
||||
expect(window.showInformationMessage).to.have.been.calledOnce;
|
||||
expect(ENABLE_TELEMETRY.getValue()).to.eq(true);
|
||||
expect(ctx.globalState.get('telemetry-request-viewed')).to.be.true;
|
||||
});
|
||||
|
||||
it('should prevent telemetry if permission is denied', async () => {
|
||||
sandbox.stub(window, 'showInformationMessage').resolvesArg(4 /* "no" item */);
|
||||
await ctx.globalState.update('telemetry-request-viewed', false);
|
||||
await enableTelemetry('codeQL.telemetry', true);
|
||||
|
||||
await telemetryListener.initialize();
|
||||
|
||||
// Dialog opened, user clicks "no" and telemetry disabled
|
||||
expect(window.showInformationMessage).to.have.been.calledOnce;
|
||||
expect(ENABLE_TELEMETRY.getValue()).to.eq(false);
|
||||
expect(ctx.globalState.get('telemetry-request-viewed')).to.be.true;
|
||||
});
|
||||
|
||||
it('should unchange telemetry if permission dialog is dismissed', async () => {
|
||||
sandbox.stub(window, 'showInformationMessage').resolves(undefined /* cancelled */);
|
||||
await ctx.globalState.update('telemetry-request-viewed', false);
|
||||
|
||||
// this causes requestTelemetryPermission to be called
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
|
||||
// Dialog opened, and user closes without interacting with it
|
||||
expect(window.showInformationMessage).to.have.been.calledOnce;
|
||||
expect(ENABLE_TELEMETRY.getValue()).to.eq(false);
|
||||
// dialog was canceled, so should not have marked as viewed
|
||||
expect(ctx.globalState.get('telemetry-request-viewed')).to.be.false;
|
||||
});
|
||||
|
||||
it('should unchange telemetry if permission dialog is cancelled if starting as true', async () => {
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
|
||||
// as before, except start with telemetry enabled. It should _stay_ enabled if the
|
||||
// dialog is canceled.
|
||||
sandbox.stub(window, 'showInformationMessage').resolves(undefined /* cancelled */);
|
||||
await ctx.globalState.update('telemetry-request-viewed', false);
|
||||
|
||||
// this causes requestTelemetryPermission to be called
|
||||
await enableTelemetry('codeQL.telemetry', true);
|
||||
|
||||
// Dialog opened, and user closes without interacting with it
|
||||
// Telemetry state should not have changed
|
||||
expect(window.showInformationMessage).to.have.been.calledOnce;
|
||||
expect(ENABLE_TELEMETRY.getValue()).to.eq(true);
|
||||
// dialog was canceled, so should not have marked as viewed
|
||||
expect(ctx.globalState.get('telemetry-request-viewed')).to.be.false;
|
||||
});
|
||||
|
||||
it('should avoid showing dialog if global telemetry is disabled', async () => {
|
||||
// when telemetry is disabled globally, we never want to show the
|
||||
// opt in/out dialog. We just assume that codeql telemetry should
|
||||
// remain disabled as well.
|
||||
// If the user ever turns global telemetry back on, then we can
|
||||
// show the dialog.
|
||||
|
||||
await enableTelemetry('telemetry', false);
|
||||
await ctx.globalState.update('telemetry-request-viewed', false);
|
||||
sandbox.stub(window, 'showInformationMessage');
|
||||
|
||||
await telemetryListener.initialize();
|
||||
|
||||
// popup should not be shown even though we have initialized telemetry
|
||||
expect(window.showInformationMessage).not.to.have.been.called;
|
||||
});
|
||||
|
||||
// This test is failing because codeQL.canary is not a registered configuration.
|
||||
// We do not want to have it registered because we don't want this item
|
||||
// appearing in the settings page. It needs to olny be set by users we tell
|
||||
// about it to.
|
||||
// At this point, I see no other way of testing re-requesting permission.
|
||||
xit('should request permission again when user changes canary setting', async () => {
|
||||
// initially, both canary and telemetry are false
|
||||
await workspace.getConfiguration().update('codeQL.canary', false);
|
||||
await enableTelemetry('codeQL.telemetry', false);
|
||||
await ctx.globalState.update('telemetry-request-viewed', true);
|
||||
await telemetryListener.initialize();
|
||||
sandbox.stub(window, 'showInformationMessage').resolves(undefined /* cancelled */);
|
||||
|
||||
// set canary to true
|
||||
await workspace.getConfiguration().update('codeQL.canary', true);
|
||||
|
||||
// now, we should have to click through the telemetry requestor again
|
||||
expect(ctx.globalState.get('telemetry-request-viewed')).to.be.false;
|
||||
expect(window.showInformationMessage).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
function createMockExtensionContext(): ExtensionContext {
|
||||
return {
|
||||
globalState: {
|
||||
_state: {
|
||||
'telemetry-request-viewed': true
|
||||
} as Record<string, any>,
|
||||
get(key: string) {
|
||||
return this._state[key];
|
||||
},
|
||||
update(key: string, val: any) {
|
||||
this._state[key] = val;
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
}
|
||||
|
||||
async function enableTelemetry(section: string, value: boolean | undefined) {
|
||||
await workspace.getConfiguration(section).update(
|
||||
'enableTelemetry', value, ConfigurationTarget.Global
|
||||
);
|
||||
|
||||
// Need to wait some time since the onDidChangeConfiguration listeners fire
|
||||
// asynchronously and we sometimes need to wait for them to complete in
|
||||
// order to have as successful test.
|
||||
await wait(50);
|
||||
}
|
||||
|
||||
async function wait(ms = 0) {
|
||||
return new Promise(resolve =>
|
||||
setTimeout(resolve, ms)
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user