forked from microsoft/vscode-cpptools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithubAPI.ts
More file actions
333 lines (301 loc) · 12.8 KB
/
githubAPI.ts
File metadata and controls
333 lines (301 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
'use strict';
import { PackageVersion } from './packageVersion';
import * as util from './common';
import { PlatformInformation } from './platform';
import { OutgoingHttpHeaders } from 'http';
import * as vscode from 'vscode';
import * as telemetry from './telemetry';
const testingInsidersVsixInstall: boolean = false; // Change this to true to enable testing of the Insiders vsix installation.
export const releaseDownloadUrl: string = "https://github.com/microsoft/vscode-cpptools/releases";
/**
* The object representation of a Build Asset. Each Asset corresponds to information about a release file on GitHub.
*/
export interface Asset {
name: string;
browser_download_url: string;
}
/**
* The object representation of a release in the GitHub API's release JSON.
* Named Build so as to reduce confusion between a "Release" release and "Insiders" release.
*/
export interface Build {
name: string;
assets: Asset[];
}
/**
* Search each Asset by name to retrieve the download URL for a VSIX package
* @param vsixName The name of the VSIX to search for
* @return The download URL of the VSIX
*/
function getVsixDownloadUrl(build: Build, vsixName: string): string {
const asset: Asset | undefined = build.assets.find(asset => asset.name === vsixName);
const downloadUrl: string | null = (asset) ? asset.browser_download_url : null;
if (!downloadUrl) {
throw new Error(`Failed to find VSIX: ${vsixName} in build: ${build.name}`);
}
return downloadUrl;
}
/**
* Determine whether an object is of type Asset.
* @param input Incoming object.
* @return Whether input is of type Asset.
*/
function isAsset(input: any): input is Asset {
return input && input.name && typeof(input.name) === "string" &&
input.browser_download_url && typeof(input.browser_download_url) === "string";
}
/**
* Determine whether an object is of type Build.
* @param input Incoming object.
* @return Whether input is of type Build.
*/
function isBuild(input: any): input is Build {
return input && input.name && typeof (input.name) === "string" && isArrayOfAssets(input.assets);
}
/**
* Determine whether an object is of type Build, and it has 3 or more assets (i.e valid build).
* Note that earlier releases of the extension do not have 3 or greater Assets
* (Mac, Win, Linux). Only call this on more recent Builds.
* @param input Incoming object.
* @return Whether input is a valid build.
*/
function isValidBuild(input: any): input is Build {
return isBuild(input) && input.assets.length >= 3;
}
/**
* Determine whether an object is of type Asset[].
* @param input Incoming object.
* @return Whether input is of type Asset[].
*/
function isArrayOfAssets(input: any): input is Asset[] {
return input instanceof Array && input.every(isAsset);
}
/**
* Return the most recent released builds.
* @param input Incoming object.
* @return An array of type Build[].
*/
function getArrayOfBuilds(input: any): Build[] {
const builds: Build[] = [];
if (!input || !(input instanceof Array) || input.length === 0) {
return builds;
}
// Only return the the most recent release and insider builds.
for (let i: number = 0; i < input.length; i++) {
if (isBuild(input[i])) {
builds.push(input[i]);
// the latest "valid" released build
if (input[i].name.indexOf('-') === -1 && isValidBuild(input[i])) {
break;
}
}
}
return builds;
}
/**
* Match the user's platform information to the VSIX name relevant to them.
* @param info Information about the user's operating system.
* @return VSIX filename for the extension's releases matched to the user's platform.
*/
export function vsixNameForPlatform(info: PlatformInformation): string {
const vsixName: string | undefined = function(platformInfo): string | undefined {
switch (platformInfo.platform) {
case 'win32':
switch (platformInfo.architecture) {
case 'x64': return 'cpptools-win32.vsix'; // TODO: Change to cpptools-win64?
case 'x86': return 'cpptools-win32.vsix';
case 'arm64': return 'cpptools-win-arm64.vsix';
default: throw new Error(`Unexpected Windows architecture: ${platformInfo.architecture}`);
}
case 'darwin':
switch (platformInfo.architecture) {
case 'x64': return 'cpptools-osx.vsix';
case 'arm64': return 'cpptools-osx-arm64.vsix';
default: throw new Error(`Unexpected macOS architecture: ${platformInfo.architecture}`);
}
default: {
switch (platformInfo.architecture) {
case 'x64': return 'cpptools-linux.vsix';
case 'arm': return 'cpptools-linux-armhf.vsix';
case 'arm64': return 'cpptools-linux-aarch64.vsix';
default: throw new Error(`Unexpected Linux architecture: ${platformInfo.architecture}`);
}
}
}
}(info);
if (!vsixName) {
throw new Error(`Failed to match VSIX name for: ${info.platform}: ${info.architecture}`);
}
return vsixName;
}
/**
* Interface for return value of getTargetBuildInfo containing the download URL and version of a Build.
*/
export interface BuildInfo {
downloadUrl: string;
name: string;
}
/**
* Use the GitHub API to retrieve the download URL of the extension version the user should update to, if any.
* @param updateChannel The user's updateChannel setting.
* @param isFromSettingsChange True if the invocation is the result of a settings change.
* @return Download URL for the extension VSIX package that the user should install. If the user
* does not need to update, resolves to undefined.
*/
export async function getTargetBuildInfo(updateChannel: string, isFromSettingsChange: boolean): Promise<BuildInfo | undefined> {
const builds: Build[] | undefined = await getReleaseJson();
if (!builds || builds.length === 0) {
return undefined;
}
const userVersion: PackageVersion = new PackageVersion(util.packageJson.version);
const targetBuild: Build | undefined = getTargetBuild(builds, userVersion, updateChannel, isFromSettingsChange);
if (targetBuild === undefined) {
// no action
telemetry.logLanguageServerEvent("UpgradeCheck", { "action": "none" });
} else if (userVersion.isExtensionVersionGreaterThan(new PackageVersion(targetBuild.name))) {
// downgrade
telemetry.logLanguageServerEvent("UpgradeCheck", { "action": "downgrade", "newVersion": targetBuild.name });
} else {
// upgrade
telemetry.logLanguageServerEvent("UpgradeCheck", { "action": "upgrade", "newVersion": targetBuild.name });
}
if (!targetBuild) {
return undefined;
}
const platformInfo: PlatformInformation = await PlatformInformation.GetPlatformInformation();
const vsixName: string = vsixNameForPlatform(platformInfo);
const downloadUrl: string = getVsixDownloadUrl(targetBuild, vsixName);
if (!downloadUrl) {
return undefined;
}
return { downloadUrl: downloadUrl, name: targetBuild.name };
}
/**
* Determines whether there exists a Build in the given Build[] that should be installed.
* @param builds The GitHub release list parsed as an array of Builds.
* @param userVersion The verion of the extension that the user is running.
* @param updateChannel The user's updateChannel setting.
* @param isFromSettingsChange True if the invocation is the result of a settings change.
* @return The Build if the user should update to it, otherwise undefined.
*/
export function getTargetBuild(builds: Build[], userVersion: PackageVersion, updateChannel: string, isFromSettingsChange: boolean): Build | undefined {
if (!isFromSettingsChange && !vscode.workspace.getConfiguration("extensions", null).get<boolean>("autoUpdate")) {
return undefined;
}
const latestVersionOnline: PackageVersion = new PackageVersion(builds[0].name);
// Allows testing pre-releases without accidentally downgrading to the latest version
if ((!testingInsidersVsixInstall && userVersion.suffix && userVersion.suffix !== 'insiders') ||
userVersion.isExtensionVersionGreaterThan(latestVersionOnline)) {
return undefined;
}
// Get predicates to determine the build to install, if any
let needsUpdate: (installed: PackageVersion, target: PackageVersion) => boolean;
let useBuild: (build: Build) => boolean;
if (updateChannel === 'Insiders') {
needsUpdate = (installed: PackageVersion, target: PackageVersion) => testingInsidersVsixInstall || (!target.isEqual(installed));
// Check if the assets are available
useBuild = isValidBuild;
} else if (updateChannel === 'Default') {
// If the updateChannel switches from 'Insiders' to 'Default', a downgrade to the latest non-insiders release is needed.
needsUpdate = function(installed: PackageVersion, target: PackageVersion): boolean {
return installed.isExtensionVersionGreaterThan(target); };
// Look for the latest non-insiders released build
useBuild = (build: Build): boolean => build.name.indexOf('-') === -1 && isValidBuild(build);
} else {
throw new Error('Incorrect updateChannel setting provided');
}
// Get the build to install
const targetBuild: Build | undefined = builds.find(useBuild);
if (!targetBuild) {
throw new Error('Failed to determine installation candidate');
}
// Check current version against target's version to determine if the installation should happen
const targetVersion: PackageVersion = new PackageVersion(targetBuild.name);
if (needsUpdate(userVersion, targetVersion)) {
return targetBuild;
} else {
return undefined;
}
}
interface Rate {
remaining: number;
}
interface RateLimit {
rate: Rate;
}
function isRate(input: any): input is Rate {
return input && util.isNumber(input.remaining);
}
function isRateLimit(input: any): input is RateLimit {
return input && isRate(input.rate);
}
async function getRateLimit(): Promise<RateLimit | undefined> {
const header: OutgoingHttpHeaders = { 'User-Agent': 'vscode-cpptools' };
try {
const data: string = await util.downloadFileToStr('https://api.github.com/rate_limit', header);
if (!data) {
return undefined;
}
let rateLimit: any;
try {
rateLimit = JSON.parse(data);
} catch (error) {
throw new Error('Failed to parse rate limit JSON');
}
if (isRateLimit(rateLimit)) {
return rateLimit;
} else {
throw new Error('Rate limit JSON is not of type RateLimit');
}
} catch (err) {
if (err && err.code && err.code !== "ENOENT") {
// Only throw if the user is connected to the Internet.
throw new Error('Failed to download rate limit JSON');
}
}
}
async function rateLimitExceeded(): Promise<boolean> {
const rateLimit: RateLimit | undefined = await getRateLimit();
return rateLimit !== undefined && rateLimit.rate.remaining <= 0;
}
/**
* Download and parse the release list JSON from the GitHub API into a Build[].
* @return Information about the released builds of the C/C++ extension.
*/
async function getReleaseJson(): Promise<Build[] | undefined> {
if (await rateLimitExceeded()) {
throw new Error('Failed to stay within GitHub API rate limit');
}
// Download release JSON
const releaseUrl: string = 'https://api.github.com/repos/Microsoft/vscode-cpptools/releases';
const header: OutgoingHttpHeaders = { 'User-Agent': 'vscode-cpptools' };
try {
const data: string = await util.downloadFileToStr(releaseUrl, header);
if (!data) {
return undefined;
}
// Parse the file
let releaseJson: any;
try {
releaseJson = JSON.parse(data);
} catch (error) {
throw new Error('Failed to parse release JSON');
}
// Find the latest released builds.
const builds: Build[] = getArrayOfBuilds(releaseJson);
if (!builds || builds.length === 0) {
throw new Error('Release JSON is not of type Build[]');
} else {
return builds;
}
} catch (err) {
if (err && err.code && err.code !== "ENOENT") {
// Only throw if the user is connected to the Internet.
throw new Error('Failed to download release JSON');
}
}
}