Compare commits

...

31 Commits

Author SHA1 Message Date
Andrew Eisenberg
27529bfc33 v1.4.2
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-02-02 14:23:49 -08:00
Andrew Eisenberg
0e4ae83e74 ` 2021-02-02 12:38:53 -08:00
Andrew Eisenberg
3b1ff0f4a3 Add a codeql status bar item
Includes the current cli version as well as the
canary status (codeQL.canary) in the settings.
2021-02-02 09:40:59 -08:00
Andrew Eisenberg
5079abd06f Fix version constraint
Non-destructive upgrades only exist in versions >= 2.4.2
2021-02-02 09:17:33 -08:00
aeisenberg
4e94f70e6f Bump version to v1.4.2 2021-01-29 21:45:42 -08:00
Andrew Eisenberg
79e2666586 v1.4.1
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-01-29 21:37:29 -08:00
Andrew Eisenberg
02080cd797 Change text and fix link of modal dialog
Modal dialogs do not allow for markdown text. The link was invalid.
Also, make CodeQL more prominent in the dialog.
2021-01-29 17:46:42 -08:00
aeisenberg
7347ff5512 Bump version to v1.4.1 2021-01-29 16:07:07 -08:00
Andrew Eisenberg
c26217df88 v1.4.0
Some checks failed
Code Scanning - CodeQL / codeql (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Publish to VS Code Marketplace (push) Has been cancelled
Release / Publish to Open VSX Registry (push) Has been cancelled
2021-01-29 15:32:50 -08:00
Andrew Eisenberg
31b445c8d2 Remove logic to only create release artifacts on PRs
Create them for appropriately named tags and workflow dispatch as well.
2021-01-29 15:21:12 -08:00
Andrew Eisenberg
7387ef6d2c Fix telemetry recording bug
When someone disables and then re-enables the global telemetry setting,
the telemetry recorder needs to be recreated in order to allow it to
respond to events again.

Also, write the telemetry log item in the same telemetry processor as
is used to remove unused fields. This ensures there is no race condition
on the order of telemetry processors being run. We always log after
fields are removed.
2021-01-29 15:21:12 -08:00
Andrew Eisenberg
091d36b1a0 Tweak telemetry page and changelog 2021-01-29 15:21:12 -08:00
Andrew Eisenberg
292e695646 Add telemetry for commands
This commit adds telemetry capturing for command execution. The data
captured explicitly captured and sent to application insights is only
the command id, execution time, and command completion status. We also
capture errors thrown by any command execution, but these are not sent
to application insights.

Telemetry capturing is opt-in. No data will be sent to application
insights unless the user explicitly allows it.

There are two new config settings added. The first controls whether or
not telemetry should be sent. This setting AND the global telemetry setting
must be enabled in order for telemetry to be sent.

The second setting controls whether or not telemetry event data should
be logged to the extension console. The hope here is that users can
inspect exactly what data is sent to the server and can have confidence
that nothing concerning is being leaked.

Note that the global setting for disabling telemetry collection is
handled inside the  `vscode-extension-telemetry` package implicitly, so
this extension doesn't touch that setting explicitly.

The `codeql.canary` setting is being used to add an additional flag to
telemetry events. This flag will help us determine if a user in internal
or not.

The application insights key is injected at build time through a
repository secret.

This commit also includes a new `TELEMETRY.md` file that explains what
is being captured, and why.
2021-01-29 15:21:12 -08:00
Andrew Eisenberg
f154206b47 Fix invalid property name on message 2021-01-29 11:24:07 -08:00
Andrew Eisenberg
07eb334e6c Ensure databases are re-registered when query server restarts
This commit fixes #733. It does it by ensuring that the query server
emits an event when it restarts the query server. The database manager
listens for this even and properly re-registers its databases.

A few caveats though:

1. Convert query restarts to using a command that includes progress.
   This will ensure that errors on restart are logged properly.
2. Because we want to log errors, we cannot use the vscode standard
   EventEmitters. They run in the next tick and therefore any errors
   will not be associated with this command execution.
3. Update the default cli version to run integration tests against to
   2.4.2.
4. Add a new integration test that fails if databases are not
   re-registered.
2021-01-29 11:24:07 -08:00
alexet
89b86055d7 Use asycy tmp 2021-01-28 16:13:33 -08:00
alexet
4dfec7014c Adress comments 2021-01-28 16:13:33 -08:00
alexet
fbff2df899 Remove unused variable 2021-01-28 16:13:33 -08:00
alexet
9cbe5ba2e8 Simplify query server interface. 2021-01-28 16:13:33 -08:00
alexet
70ddbd05be Adress comments on non-destructive upgrades. 2021-01-28 16:13:33 -08:00
alexet
ace92a4674 Remove uneeded argument 2021-01-28 16:13:33 -08:00
alexet
24b3e158b7 Set codeql version to required version. 2021-01-28 16:13:33 -08:00
alexet
a399041cba Fix rebase conflict 2021-01-28 16:13:33 -08:00
alexet
676546d32b Adress review comments 2021-01-28 16:13:33 -08:00
alexet
a25db9616f QueryServer: Use non-destructive upgrades where possible. 2021-01-28 16:13:33 -08:00
alexet
cb4d6f228b QueryServer: Add new commands to client. 2021-01-28 16:13:33 -08:00
alexet
424884b6b1 Add support for new cli feature 2021-01-28 16:13:33 -08:00
Henry Mercer
f741deb48b Forward scored query metadata property for canary users 2021-01-21 19:36:34 +00:00
Henry Mercer
ae6be79c51 Add config setting to enable canary features 2021-01-21 19:36:34 +00:00
Henry Mercer
154b4a2fe2 Fix missing call to showAndLogErrorMessage 2021-01-21 11:34:30 -08:00
aeisenberg
650f4ca047 Bump version to v1.3.11 2021-01-21 19:06:58 +00:00
39 changed files with 1618 additions and 301 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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`

View File

@@ -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)

View File

@@ -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.

View 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 [Microsofts 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.

View 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/'));
}

View File

@@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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);
}
});
}

View File

@@ -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>();
}

View File

@@ -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
);

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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());

View File

@@ -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.

View File

@@ -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';

View File

@@ -34,6 +34,7 @@ export interface QueryMetadata {
description?: string;
id?: string;
kind?: string;
scored?: string;
}
export interface PreviousExecution {

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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,

View 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();
}
}

View 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);
}

View File

@@ -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(

View File

@@ -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);
}
});
});

View File

@@ -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, () => { /**/ });

View File

@@ -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

View File

@@ -66,7 +66,8 @@ describe('databases', () => {
} as unknown as ExtensionContext,
{
sendRequest: sendRequestSpy,
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy
supportsDatabaseRegistration: supportsDatabaseRegistrationSpy,
onDidStartQueryServer: () => { /**/ }
} as unknown as QueryServerClient,
{
supportsLanguageName: supportsLanguageNameSpy,

View File

@@ -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/);
});

View File

@@ -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) }

View File

@@ -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();

View File

@@ -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));
});
});
});

View File

@@ -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
);

View File

@@ -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)
);
}
});