11import { field , logger } from "@coder/logger"
2- import * as cp from "child_process"
3- import * as fs from "fs-extra"
42import * as http from "http"
53import * as https from "https"
6- import * as os from "os"
74import * as path from "path"
85import * as semver from "semver"
9- import { Readable , Writable } from "stream"
10- import * as tar from "tar-fs"
116import * as url from "url"
12- import * as util from "util"
13- import * as zlib from "zlib"
147import { HttpCode , HttpError } from "../../common/http"
158import { HttpProvider , HttpProviderOptions , HttpResponse , Route } from "../http"
169import { settings as globalSettings , SettingsProvider , UpdateSettings } from "../settings"
17- import { tmpdir } from "../util"
18- import { ipcMain } from "../wrapper"
1910
2011export interface Update {
2112 checked : number
@@ -27,7 +18,7 @@ export interface LatestResponse {
2718}
2819
2920/**
30- * Update HTTP provider.
21+ * HTTP provider for checking updates (does not download/install them) .
3122 */
3223export class UpdateHttpProvider extends HttpProvider {
3324 private update ?: Promise < Update >
@@ -41,12 +32,6 @@ export class UpdateHttpProvider extends HttpProvider {
4132 * that fulfills `LatestResponse`.
4233 */
4334 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}}" ,
5035 /**
5136 * Update information will be stored here. If not provided, the global
5237 * settings will be used.
@@ -64,66 +49,30 @@ export class UpdateHttpProvider extends HttpProvider {
6449 throw new HttpError ( "Not found" , HttpCode . NotFound )
6550 }
6651
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" )
8154 }
8255
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" )
9360 return {
9461 content : {
95- isLatest : true ,
62+ ...update ,
63+ isLatest : this . isLatestVersion ( update ) ,
9664 } ,
9765 }
9866 }
99- const update = await this . getUpdate ( )
100- return {
101- content : {
102- ...update ,
103- isLatest : this . isLatestVersion ( update ) ,
104- } ,
105- }
10667 }
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 )
11770 }
11871
11972 /**
12073 * Query for and return the latest update.
12174 */
12275 public async getUpdate ( force ?: boolean ) : Promise < Update > {
123- if ( ! this . enabled ) {
124- throw new Error ( "updates are not enabled" )
125- }
126-
12776 // Don't run multiple requests at a time.
12877 if ( ! this . update ) {
12978 this . update = this . _getUpdate ( force )
@@ -171,128 +120,6 @@ export class UpdateHttpProvider extends HttpProvider {
171120 }
172121 }
173122
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-
296123 private async request ( uri : string ) : Promise < Buffer > {
297124 const response = await this . requestResponse ( uri )
298125 return new Promise ( ( resolve , reject ) => {
0 commit comments