1
1
import { field , logger } from "@coder/logger"
2
- import * as cp from "child_process"
3
- import * as fs from "fs-extra"
4
2
import * as http from "http"
5
3
import * as https from "https"
6
- import * as os from "os"
7
4
import * as path from "path"
8
5
import * as semver from "semver"
9
- import { Readable , Writable } from "stream"
10
- import * as tar from "tar-fs"
11
6
import * as url from "url"
12
- import * as util from "util"
13
- import * as zlib from "zlib"
14
7
import { HttpCode , HttpError } from "../../common/http"
15
8
import { HttpProvider , HttpProviderOptions , HttpResponse , Route } from "../http"
16
9
import { settings as globalSettings , SettingsProvider , UpdateSettings } from "../settings"
17
- import { tmpdir } from "../util"
18
- import { ipcMain } from "../wrapper"
19
10
20
11
export interface Update {
21
12
checked : number
@@ -27,7 +18,7 @@ export interface LatestResponse {
27
18
}
28
19
29
20
/**
30
- * Update HTTP provider.
21
+ * HTTP provider for checking updates (does not download/install them) .
31
22
*/
32
23
export class UpdateHttpProvider extends HttpProvider {
33
24
private update ?: Promise < Update >
@@ -41,12 +32,6 @@ export class UpdateHttpProvider extends HttpProvider {
41
32
* that fulfills `LatestResponse`.
42
33
*/
43
34
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest" ,
44
- /**
45
- * The URL for downloading a version of code-server. {{VERSION}} and
46
- * {{RELEASE_NAME}} will be replaced (for example 2.1.0 and
47
- * code-server-2.1.0-linux-x86_64.tar.gz).
48
- */
49
- private readonly downloadUrl = "https://github.com/cdr/code-server/releases/download/{{VERSION}}/{{RELEASE_NAME}}" ,
50
35
/**
51
36
* Update information will be stored here. If not provided, the global
52
37
* settings will be used.
@@ -64,66 +49,30 @@ export class UpdateHttpProvider extends HttpProvider {
64
49
throw new HttpError ( "Not found" , HttpCode . NotFound )
65
50
}
66
51
67
- switch ( route . base ) {
68
- case "/check" :
69
- this . getUpdate ( true )
70
- if ( route . query && route . query . to ) {
71
- return {
72
- redirect : Array . isArray ( route . query . to ) ? route . query . to [ 0 ] : route . query . to ,
73
- query : { to : undefined } ,
74
- }
75
- }
76
- return this . getRoot ( route , request )
77
- case "/apply" :
78
- return this . tryUpdate ( route , request )
79
- case "/" :
80
- return this . getRoot ( route , request )
52
+ if ( ! this . enabled ) {
53
+ throw new Error ( "update checks are disabled" )
81
54
}
82
55
83
- throw new HttpError ( "Not found" , HttpCode . NotFound )
84
- }
85
-
86
- public async getRoot (
87
- route : Route ,
88
- request : http . IncomingMessage ,
89
- errorOrUpdate ?: Update | Error ,
90
- ) : Promise < HttpResponse > {
91
- if ( request . headers [ "content-type" ] === "application/json" ) {
92
- if ( ! this . enabled ) {
56
+ switch ( route . base ) {
57
+ case "/check" :
58
+ case "/" : {
59
+ const update = await this . getUpdate ( route . base === "/check" )
93
60
return {
94
61
content : {
95
- isLatest : true ,
62
+ ...update ,
63
+ isLatest : this . isLatestVersion ( update ) ,
96
64
} ,
97
65
}
98
66
}
99
- const update = await this . getUpdate ( )
100
- return {
101
- content : {
102
- ...update ,
103
- isLatest : this . isLatestVersion ( update ) ,
104
- } ,
105
- }
106
67
}
107
- const response = await this . getUtf8Resource ( this . rootPath , "src/browser/pages/update.html" )
108
- response . content = response . content
109
- . replace (
110
- / { { UPDATE_ S T A T U S } } / ,
111
- errorOrUpdate && ! ( errorOrUpdate instanceof Error )
112
- ? `Updated to ${ errorOrUpdate . version } `
113
- : await this . getUpdateHtml ( ) ,
114
- )
115
- . replace ( / { { ERROR} } / , errorOrUpdate instanceof Error ? `<div class="error">${ errorOrUpdate . message } </div>` : "" )
116
- return this . replaceTemplates ( route , response )
68
+
69
+ throw new HttpError ( "Not found" , HttpCode . NotFound )
117
70
}
118
71
119
72
/**
120
73
* Query for and return the latest update.
121
74
*/
122
75
public async getUpdate ( force ?: boolean ) : Promise < Update > {
123
- if ( ! this . enabled ) {
124
- throw new Error ( "updates are not enabled" )
125
- }
126
-
127
76
// Don't run multiple requests at a time.
128
77
if ( ! this . update ) {
129
78
this . update = this . _getUpdate ( force )
@@ -171,128 +120,6 @@ export class UpdateHttpProvider extends HttpProvider {
171
120
}
172
121
}
173
122
174
- private async getUpdateHtml ( ) : Promise < string > {
175
- if ( ! this . enabled ) {
176
- return "Updates are disabled"
177
- }
178
-
179
- const update = await this . getUpdate ( )
180
- if ( this . isLatestVersion ( update ) ) {
181
- return "No update available"
182
- }
183
-
184
- return `<button type="submit" class="apply -button">Update to ${ update . version } </button>`
185
- }
186
-
187
- public async tryUpdate ( route : Route , request : http . IncomingMessage ) : Promise < HttpResponse > {
188
- try {
189
- const update = await this . getUpdate ( )
190
- if ( ! this . isLatestVersion ( update ) ) {
191
- await this . downloadAndApplyUpdate ( update )
192
- return this . getRoot ( route , request , update )
193
- }
194
- return this . getRoot ( route , request )
195
- } catch ( error ) {
196
- // For JSON requests propagate the error. Otherwise catch it so we can
197
- // show the error inline with the update button instead of an error page.
198
- if ( request . headers [ "content-type" ] === "application/json" ) {
199
- throw error
200
- }
201
- return this . getRoot ( route , error )
202
- }
203
- }
204
-
205
- public async downloadAndApplyUpdate ( update : Update , targetPath ?: string ) : Promise < void > {
206
- const releaseName = await this . getReleaseName ( update )
207
- const url = this . downloadUrl . replace ( "{{VERSION}}" , update . version ) . replace ( "{{RELEASE_NAME}}" , releaseName )
208
-
209
- let downloadPath = path . join ( tmpdir , "updates" , releaseName )
210
- fs . mkdirp ( path . dirname ( downloadPath ) )
211
-
212
- const response = await this . requestResponse ( url )
213
-
214
- try {
215
- downloadPath = await this . extractTar ( response , downloadPath )
216
- logger . debug ( "Downloaded update" , field ( "path" , downloadPath ) )
217
-
218
- // The archive should have a directory inside at the top level with the
219
- // same name as the archive.
220
- const directoryPath = path . join ( downloadPath , path . basename ( downloadPath ) )
221
- await fs . stat ( directoryPath )
222
-
223
- if ( ! targetPath ) {
224
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
225
- targetPath = path . resolve ( __dirname , "../../../" )
226
- }
227
-
228
- // Move the old directory to prevent potential data loss.
229
- const backupPath = path . resolve ( targetPath , `../${ path . basename ( targetPath ) } .${ Date . now ( ) . toString ( ) } ` )
230
- logger . debug ( "Replacing files" , field ( "target" , targetPath ) , field ( "backup" , backupPath ) )
231
- await fs . move ( targetPath , backupPath )
232
-
233
- // Move the new directory.
234
- await fs . move ( directoryPath , targetPath )
235
-
236
- await fs . remove ( downloadPath )
237
-
238
- if ( process . send ) {
239
- ipcMain ( ) . relaunch ( update . version )
240
- }
241
- } catch ( error ) {
242
- response . destroy ( error )
243
- throw error
244
- }
245
- }
246
-
247
- private async extractTar ( response : Readable , downloadPath : string ) : Promise < string > {
248
- downloadPath = downloadPath . replace ( / \. t a r \. g z $ / , "" )
249
- logger . debug ( "Extracting tar" , field ( "path" , downloadPath ) )
250
-
251
- response . pause ( )
252
- await fs . remove ( downloadPath )
253
-
254
- const decompress = zlib . createGunzip ( )
255
- response . pipe ( decompress as Writable )
256
- response . on ( "error" , ( error ) => decompress . destroy ( error ) )
257
- response . on ( "close" , ( ) => decompress . end ( ) )
258
-
259
- const destination = tar . extract ( downloadPath )
260
- decompress . pipe ( destination )
261
- decompress . on ( "error" , ( error ) => destination . destroy ( error ) )
262
- decompress . on ( "close" , ( ) => destination . end ( ) )
263
-
264
- await new Promise ( ( resolve , reject ) => {
265
- destination . on ( "finish" , resolve )
266
- destination . on ( "error" , reject )
267
- response . resume ( )
268
- } )
269
-
270
- return downloadPath
271
- }
272
-
273
- /**
274
- * Given an update return the name for the packaged archived.
275
- */
276
- public async getReleaseName ( update : Update ) : Promise < string > {
277
- let target : string = os . platform ( )
278
- if ( target === "linux" ) {
279
- const result = await util
280
- . promisify ( cp . exec ) ( "ldd --version" )
281
- . catch ( ( error ) => ( {
282
- stderr : error . message ,
283
- stdout : "" ,
284
- } ) )
285
- if ( / m u s l / . test ( result . stderr ) || / m u s l / . test ( result . stdout ) ) {
286
- target = "alpine"
287
- }
288
- }
289
- let arch = os . arch ( )
290
- if ( arch === "x64" ) {
291
- arch = "x86_64"
292
- }
293
- return `code-server-${ update . version } -${ target } -${ arch } .tar.gz`
294
- }
295
-
296
123
private async request ( uri : string ) : Promise < Buffer > {
297
124
const response = await this . requestResponse ( uri )
298
125
return new Promise ( ( resolve , reject ) => {
0 commit comments