diff --git a/package.json b/package.json index 9832b99..c416a32 100644 --- a/package.json +++ b/package.json @@ -265,6 +265,22 @@ "default": true, "description": "Send TLC output to .out file." }, + "tlaplus.tlc.statisticsSharing": { + "type": "string", + "scope": "application", + "default": "doNotShare", + "markdownDescription": "Allows you to send the TLC usage statistics to [the plublicly available database](https://exec-stats.tlapl.us). You can find more details on what is shared [here](https://github.com/alygin/vscode-tlaplus/wiki/TLC-Statistics-Sharing).", + "enum": [ + "share", + "shareWithoutId", + "doNotShare" + ], + "enumDescriptions": [ + "TLC statistics will be shared along with the installation ID.", + "TLC statistics will be shared witout the installation ID.", + "No TLC statistics will be shared." + ] + }, "tlaplus.pdf.convertCommand": { "default": "pdflatex", "type": "string", diff --git a/src/commands/checkModel.ts b/src/commands/checkModel.ts index 187a95f..1712b34 100644 --- a/src/commands/checkModel.ts +++ b/src/commands/checkModel.ts @@ -10,6 +10,7 @@ import { saveStreamToFile } from '../outputSaver'; import { replaceExtension, LANG_TLAPLUS, LANG_TLAPLUS_CFG, listFiles, exists } from '../common'; import { ModelCheckResultSource, ModelCheckResult } from '../model/check'; import { ToolOutputChannel } from '../outputChannels'; +import { processTlcStatisticsSetting } from './tlcStatisticsCfg'; export const CMD_CHECK_MODEL_RUN = 'tlaplus.model.check.run'; export const CMD_CHECK_MODEL_CUSTOM_RUN = 'tlaplus.model.check.customRun'; @@ -53,6 +54,7 @@ export async function checkModel(diagnostic: vscode.DiagnosticCollection, extCon if (!specFiles) { return; } + await processTlcStatisticsSetting(); doCheckModel(specFiles, false, extContext, diagnostic); } diff --git a/src/commands/tlcStatisticsCfg.ts b/src/commands/tlcStatisticsCfg.ts new file mode 100644 index 0000000..de0dbc1 --- /dev/null +++ b/src/commands/tlcStatisticsCfg.ts @@ -0,0 +1,73 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { homedir } from 'os'; +import { exists, readFile, writeFile, mkDir } from '../common'; + +const CFG_TLC_STATISTICS_TYPE = 'tlaplus.tlc.statisticsSharing'; +const STAT_SETTINGS_DIR = '.tlaplus'; +const STAT_SETTINGS_FILE = 'esc.txt'; +const STAT_OPT_SHARE = '*'; +const STAT_OPT_SHARE_NO_ID = 'RANDOM_IDENTIFIER'; +const STAT_OPT_DO_NOT_SHARE = 'NO_STATISTICS'; + +let lastStatCfgValue: string | undefined; + +/** + * Checks the TLC statistics collection setting, rewrites the statistics config file if necessary. + * The statistics config file contains one of the following: + * - {installation ID} - when the user allows to share statistics along with the installation ID. + * - NO_STATISTICS - when the user doesn't want to share statistics. + * - RANDOM_IDENTIFIER - when the user allows to share statistics but without the installation ID. + * TLC handles this file automatically when running. + */ +export async function processTlcStatisticsSetting() { + const statCfg = vscode.workspace.getConfiguration().get(CFG_TLC_STATISTICS_TYPE); + if (statCfg === lastStatCfgValue) { + return; + } + lastStatCfgValue = statCfg; + switch (statCfg) { + case 'share': + setStatisticsSetting(STAT_OPT_SHARE); + break; + case 'shareWithoutId': + setStatisticsSetting(STAT_OPT_SHARE_NO_ID); + break; + case 'doNotShare': + setStatisticsSetting(STAT_OPT_DO_NOT_SHARE); + break; + default: + console.error('Unsupported TLC statistics option: ' + statCfg); + return; + } +} + +async function setStatisticsSetting(option: string) { + const dir = path.join(homedir(), STAT_SETTINGS_DIR); + const file = path.join(dir, STAT_SETTINGS_FILE); + if (await exists(file)) { + const curOption = await readFile(file); + if ((option === STAT_OPT_DO_NOT_SHARE || option === STAT_OPT_SHARE_NO_ID) && curOption.startsWith(option)) { + return; + } + if (option === STAT_OPT_SHARE + && !curOption.startsWith(STAT_OPT_DO_NOT_SHARE) + && !curOption.startsWith(STAT_OPT_SHARE_NO_ID)) { + return; + } + } + const fileContents = option === STAT_OPT_SHARE ? generateRandomInstallationId() : option; + if (!await exists(dir)) { + await mkDir(dir); + } + await writeFile(file, fileContents + '\n'); +} + +function generateRandomInstallationId(): string { + const id = []; + for (let i = 0; i < 32; i++) { + const n = i % 2; + id.push((n ^ Math.random() * 16 >> n / 4).toString(16)); + } + return id.join(''); +} diff --git a/src/common.ts b/src/common.ts index d5d17fd..138ed93 100644 --- a/src/common.ts +++ b/src/common.ts @@ -59,6 +59,18 @@ export function createTempDirSync(): string | undefined { return undefined; } +export async function mkDir(dirPath: string) { + return new Promise((resolve, reject) => { + fs.mkdir(dirPath, null, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + export async function deleteDir(dirPath: string) { for (const fileName of fs.readdirSync(dirPath)) { const filePath = path.join(dirPath, fileName);