Skip to content

Commit

Permalink
Trigger life cycle events on Git Graph install, update, or uninstall,…
Browse files Browse the repository at this point in the history
… to allow the extension to utilise the latest features of Visual Studio Code as soon as the majority of users are using a compatible version.

Full details are available at: https://api.mhutchie.com/vscode-git-graph/about
  • Loading branch information
mhutchie committed Apr 5, 2020
1 parent ece1d33 commit 1c4cd1d
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 14 deletions.
25 changes: 12 additions & 13 deletions .vscode/clean.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
const fs = require('fs');
const path = require('path');

const OUTPUT_DIRECTORY = './out';
const ASKPASS_DIRECTORY = '/askpass';

if (fs.existsSync(OUTPUT_DIRECTORY)) {
fs.readdirSync(OUTPUT_DIRECTORY).forEach(filename => {
if (filename !== 'askpass') {
fs.unlinkSync(path.join(OUTPUT_DIRECTORY, filename));
}
});

if (fs.existsSync(OUTPUT_DIRECTORY + ASKPASS_DIRECTORY)) {
fs.readdirSync(OUTPUT_DIRECTORY + ASKPASS_DIRECTORY).forEach(filename => {
fs.unlinkSync(path.join(OUTPUT_DIRECTORY + ASKPASS_DIRECTORY, filename));
function deleteFolderAndFiles(directory) {
if (fs.existsSync(directory)) {
fs.readdirSync(directory).forEach((filename) => {
const fullPath = path.join(directory, filename);
if (fs.statSync(fullPath).isDirectory()) {
deleteFolderAndFiles(fullPath);
} else {
fs.unlinkSync(fullPath);
}
});
fs.rmdirSync(directory);
}
}

deleteFolderAndFiles('./out');
2 changes: 2 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
node_modules/*/*.md
out/askpass/*.d.ts
out/askpass/*.js.map
out/life-cycle/*.d.ts
out/life-cycle/*.js.map
out/*.d.ts
out/*.js.map
resources/demo.gif
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,7 @@
},
"scripts": {
"vscode:prepublish": "npm run compile",
"vscode:uninstall": "node ./out/life-cycle/uninstall.js",
"clean": "node ./.vscode/clean.js",
"compile": "npm run clean && npm run compile-src && npm run compile-web",
"compile-src": "tsc -p ./src && node ./.vscode/package-src.js",
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DataSource } from './dataSource';
import { DiffDocProvider } from './diffDocProvider';
import { EventEmitter } from './event';
import { ExtensionState } from './extensionState';
import { onStartUp } from './life-cycle/startup';
import { Logger } from './logger';
import { RepoManager } from './repoManager';
import { StatusBarItem } from './statusBarItem';
Expand Down Expand Up @@ -79,6 +80,7 @@ export async function activate(context: vscode.ExtensionContext) {
logger.log('Started Git Graph - Ready to use!');

extensionState.expireOldCodeReviews();
onStartUp(context).catch(() => { });
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/extensionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ export class ExtensionState implements vscode.Disposable {
} else {
fs.mkdir(this.globalStoragePath, () => {
fs.mkdir(this.globalStoragePath + AVATAR_STORAGE_FOLDER, (err) => {
if (!err) this.avatarStorageAvailable = true;
if (!err || err.code === 'EEXIST') {
// The directory was created, or it already exists
this.avatarStorageAvailable = true;
}
});
});
}
Expand Down
157 changes: 157 additions & 0 deletions src/life-cycle/startup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Git Graph generates an event when it is installed, updated, or uninstalled, that is anonymous, non-personal, and cannot be correlated.
* - Each event only contains the Git Graph and Visual Studio Code version numbers, and a 256 bit cryptographically strong pseudo-random nonce.
* - The two version numbers recorded in these events only allow aggregate compatibility information to be generated (e.g. 50% of users are
* using Visual Studio Code >= 1.41.0). These insights enable Git Graph to utilise the latest features of Visual Studio Code as soon as
* the majority of users are using a compatible version. The data cannot, and will not, be used for any other purpose.
* - Full details are available at: https://api.mhutchie.com/vscode-git-graph/about
*/

import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { generateNonce, getDataDirectory, getLifeCycleStateInDirectory, LifeCycleStage, LifeCycleState, saveLifeCycleStateInDirectory, sendQueue } from './utils';

/**
* Run on startup to detect if Git Graph has been installed or updated, and if so generate an event.
* @param extensionContext The extension context of Git Graph.
*/
export async function onStartUp(extensionContext: vscode.ExtensionContext) {
if (vscode.env.sessionId === 'someValue.sessionId') {
// Extension is running in the Extension Development Host, don't proceed.
return;
}

let state = await getLifeCycleStateInDirectory(extensionContext.globalStoragePath);

if (state !== null && !state.apiAvailable) {
// The API is no longer available, don't proceed.
return;
}

const versions = {
extension: await getExtensionVersion(extensionContext),
vscode: vscode.version
};

if (state === null || state.current.extension !== versions.extension) {
// This is the first startup after installing Git Graph, or Git Graph has been updated since the last startup.
const nonce = await getNonce();

if (state === null) {
// Install
state = {
previous: null,
current: versions,
apiAvailable: true,
queue: [{
stage: LifeCycleStage.Install,
extension: versions.extension,
vscode: versions.vscode,
nonce: nonce
}]
};
} else {
// Update
state.previous = state.current;
state.current = versions;
state.queue.push({
stage: LifeCycleStage.Update,
from: state.previous,
to: state.current,
nonce: nonce
});
}

await saveLifeCycleState(extensionContext, state);
state.apiAvailable = await sendQueue(state.queue);
state.queue = [];
await saveLifeCycleState(extensionContext, state);

} else if (state.queue.length > 0) {
// There are one or more events in the queue that previously failed to send, send them
state.apiAvailable = await sendQueue(state.queue);
state.queue = [];
await saveLifeCycleState(extensionContext, state);
}
}

/**
* Saves the life cycle state to the extensions global storage directory (for use during future updates),
* and to a directory in this Git Graph installation (for use during future uninstalls).
* @param extensionContext The extension context of Git Graph.
* @param state The state to save.
*/
function saveLifeCycleState(extensionContext: vscode.ExtensionContext, state: LifeCycleState) {
return Promise.all([
saveLifeCycleStateInDirectory(extensionContext.globalStoragePath, state),
saveLifeCycleStateInDirectory(getDataDirectory(), state)
]);
}

/**
* Gets the version of Git Graph.
* @param extensionContext The extension context of Git Graph.
* @returns The Git Graph version.
*/
function getExtensionVersion(extensionContext: vscode.ExtensionContext) {
return new Promise<string>((resolve, reject) => {
fs.readFile(path.join(extensionContext.extensionPath, 'package.json'), (err, data) => {
if (err) {
reject();
} else {
try {
resolve(JSON.parse(data.toString()).version);
} catch (_) {
reject();
}
}
});
});
}

/**
* Get a nonce generated for this installation of Git Graph.
* @returns A 256 bit cryptographically strong pseudo-random nonce.
*/
function getNonce() {
return new Promise<string>((resolve, reject) => {
const dir = getDataDirectory();
const file = path.join(dir, 'lock.json');
fs.mkdir(dir, (err) => {
if (err) {
if (err.code === 'EEXIST') {
// The directory already exists, attempt to read the previously created data
fs.readFile(file, (err, data) => {
if (err) {
// Unable to read the file, reject
reject();
} else {
try {
// Resolve to the previously generated nonce
resolve(JSON.parse(data.toString()).nonce);
} catch (_) {
reject();
}
}
});
} else {
// An unexpected error occurred, reject
reject();
}
} else {
// The directory was created, generate a nonce
const nonce = generateNonce();
fs.writeFile(file, JSON.stringify({ nonce: nonce }), (err) => {
if (err) {
// Unable to save data
reject();
} else {
// Nonce successfully saved, resolve to it
resolve(nonce);
}
});
}
});
});
}
27 changes: 27 additions & 0 deletions src/life-cycle/uninstall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Git Graph generates an event when it is installed, updated, or uninstalled, that is anonymous, non-personal, and cannot be correlated.
* - Each event only contains the Git Graph and Visual Studio Code version numbers, and a 256 bit cryptographically strong pseudo-random nonce.
* - The two version numbers recorded in these events only allow aggregate compatibility information to be generated (e.g. 50% of users are
* using Visual Studio Code >= 1.41.0). These insights enable Git Graph to utilise the latest features of Visual Studio Code as soon as
* the majority of users are using a compatible version. The data cannot, and will not, be used for any other purpose.
* - Full details are available at: https://api.mhutchie.com/vscode-git-graph/about
*/

import { generateNonce, getDataDirectory, getLifeCycleStateInDirectory, LifeCycleStage, sendQueue } from './utils';

(async function () {
try {
const state = await getLifeCycleStateInDirectory(getDataDirectory());
if (state !== null) {
if (state.apiAvailable) {
state.queue.push({
stage: LifeCycleStage.Uninstall,
extension: state.current.extension,
vscode: state.current.vscode,
nonce: generateNonce()
});
await sendQueue(state.queue);
}
}
} catch (_) { }
})();
Loading

0 comments on commit 1c4cd1d

Please sign in to comment.