diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-03-19 08:06:48 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-03-19 08:06:48 +0000 |
commit | aca3c3086ee99f5770a60970e20af640c895d42a (patch) | |
tree | 2f0b3233cc4728436ba5e47a6e91e7df33585d43 /editors/code/src/net.ts | |
parent | 55336722b3662cbdcc9e1b92a3e27ed0442d2452 (diff) | |
parent | fb6e655de8a44c65275ad45a27bf5bd684670ba0 (diff) |
Merge #3629
3629: Alternative aproach to plugin auto update r=matklad a=matklad
This is very much WIP (as in, I haven't run this once), but I like the result so far.
cc @Veetaha
The primary focus here on simplification:
* local simplification of data structures and control-flow: using union of strings instead of an enum, using unwrapped GitHub API responses
* global simplification of control flow: all logic is now in `main.ts`, implemented as linear functions without abstractions. This is stateful side-effective code, so arguments from [Carmack](http://number-none.com/blow/john_carmack_on_inlined_code.html) very much apply. We need all user interractions, all mutations, and all network requests to happen in a single file.
* as a side-effect of condensing everything to functions, we can get rid of various enums. The enums were basically a reified control flow:
```
enum E { A, B }
fn foo() -> E {
if cond { E::A } else { E::B }
}
fn bar(e: E) {
match e {
E::A => do_a(),
E::B => do_b(),
}
}
==>>
fn all() {
if cond { do_a() } else { do_b() }
}
```
* simplification of model: we don't need to reinstall on settings update, we can just ask the user to reload, we don't need to handle nightly=>stable fallback, we can ask the user to reinstall extension, (todo) we don't need to parse out the date from the version, we can use build id for nightly and for stable we can write the info directly into package.json.
Co-authored-by: Aleksey Kladov <[email protected]>
Diffstat (limited to 'editors/code/src/net.ts')
-rw-r--r-- | editors/code/src/net.ts | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/editors/code/src/net.ts b/editors/code/src/net.ts new file mode 100644 index 000000000..492213937 --- /dev/null +++ b/editors/code/src/net.ts | |||
@@ -0,0 +1,131 @@ | |||
1 | import fetch from "node-fetch"; | ||
2 | import * as vscode from "vscode"; | ||
3 | import * as fs from "fs"; | ||
4 | import * as stream from "stream"; | ||
5 | import * as util from "util"; | ||
6 | import { log, assert } from "./util"; | ||
7 | |||
8 | const pipeline = util.promisify(stream.pipeline); | ||
9 | |||
10 | const GITHUB_API_ENDPOINT_URL = "https://api.github.com"; | ||
11 | const OWNER = "rust-analyzer"; | ||
12 | const REPO = "rust-analyzer"; | ||
13 | |||
14 | export async function fetchRelease( | ||
15 | releaseTag: string | ||
16 | ): Promise<GithubRelease> { | ||
17 | |||
18 | const apiEndpointPath = `/repos/${OWNER}/${REPO}/releases/tags/${releaseTag}`; | ||
19 | |||
20 | const requestUrl = GITHUB_API_ENDPOINT_URL + apiEndpointPath; | ||
21 | |||
22 | log.debug("Issuing request for released artifacts metadata to", requestUrl); | ||
23 | |||
24 | const response = await fetch(requestUrl, { headers: { Accept: "application/vnd.github.v3+json" } }); | ||
25 | |||
26 | if (!response.ok) { | ||
27 | log.error("Error fetching artifact release info", { | ||
28 | requestUrl, | ||
29 | releaseTag, | ||
30 | response: { | ||
31 | headers: response.headers, | ||
32 | status: response.status, | ||
33 | body: await response.text(), | ||
34 | } | ||
35 | }); | ||
36 | |||
37 | throw new Error( | ||
38 | `Got response ${response.status} when trying to fetch ` + | ||
39 | `release info for ${releaseTag} release` | ||
40 | ); | ||
41 | } | ||
42 | |||
43 | // We skip runtime type checks for simplicity (here we cast from `any` to `GithubRelease`) | ||
44 | const release: GithubRelease = await response.json(); | ||
45 | return release; | ||
46 | } | ||
47 | |||
48 | // We omit declaration of tremendous amount of fields that we are not using here | ||
49 | export interface GithubRelease { | ||
50 | name: string; | ||
51 | id: number; | ||
52 | // eslint-disable-next-line camelcase | ||
53 | published_at: string; | ||
54 | assets: Array<{ | ||
55 | name: string; | ||
56 | // eslint-disable-next-line camelcase | ||
57 | browser_download_url: string; | ||
58 | }>; | ||
59 | } | ||
60 | |||
61 | |||
62 | export async function download( | ||
63 | downloadUrl: string, | ||
64 | destinationPath: string, | ||
65 | progressTitle: string, | ||
66 | { mode }: { mode?: number } = {}, | ||
67 | ) { | ||
68 | await vscode.window.withProgress( | ||
69 | { | ||
70 | location: vscode.ProgressLocation.Notification, | ||
71 | cancellable: false, | ||
72 | title: progressTitle | ||
73 | }, | ||
74 | async (progress, _cancellationToken) => { | ||
75 | let lastPercentage = 0; | ||
76 | await downloadFile(downloadUrl, destinationPath, mode, (readBytes, totalBytes) => { | ||
77 | const newPercentage = (readBytes / totalBytes) * 100; | ||
78 | progress.report({ | ||
79 | message: newPercentage.toFixed(0) + "%", | ||
80 | increment: newPercentage - lastPercentage | ||
81 | }); | ||
82 | |||
83 | lastPercentage = newPercentage; | ||
84 | }); | ||
85 | } | ||
86 | ); | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Downloads file from `url` and stores it at `destFilePath` with `destFilePermissions`. | ||
91 | * `onProgress` callback is called on recieveing each chunk of bytes | ||
92 | * to track the progress of downloading, it gets the already read and total | ||
93 | * amount of bytes to read as its parameters. | ||
94 | */ | ||
95 | async function downloadFile( | ||
96 | url: string, | ||
97 | destFilePath: fs.PathLike, | ||
98 | mode: number | undefined, | ||
99 | onProgress: (readBytes: number, totalBytes: number) => void | ||
100 | ): Promise<void> { | ||
101 | const res = await fetch(url); | ||
102 | |||
103 | if (!res.ok) { | ||
104 | log.error("Error", res.status, "while downloading file from", url); | ||
105 | log.error({ body: await res.text(), headers: res.headers }); | ||
106 | |||
107 | throw new Error(`Got response ${res.status} when trying to download a file.`); | ||
108 | } | ||
109 | |||
110 | const totalBytes = Number(res.headers.get('content-length')); | ||
111 | assert(!Number.isNaN(totalBytes), "Sanity check of content-length protocol"); | ||
112 | |||
113 | log.debug("Downloading file of", totalBytes, "bytes size from", url, "to", destFilePath); | ||
114 | |||
115 | let readBytes = 0; | ||
116 | res.body.on("data", (chunk: Buffer) => { | ||
117 | readBytes += chunk.length; | ||
118 | onProgress(readBytes, totalBytes); | ||
119 | }); | ||
120 | |||
121 | const destFileStream = fs.createWriteStream(destFilePath, { mode }); | ||
122 | |||
123 | await pipeline(res.body, destFileStream); | ||
124 | return new Promise<void>(resolve => { | ||
125 | destFileStream.on("close", resolve); | ||
126 | destFileStream.destroy(); | ||
127 | |||
128 | // Details on workaround: https://github.com/rust-analyzer/rust-analyzer/pull/3092#discussion_r378191131 | ||
129 | // Issue at nodejs repo: https://github.com/nodejs/node/issues/31776 | ||
130 | }); | ||
131 | } | ||