11import axios from "axios"
22import { execFile } from "child_process"
33import { getBuildInfo } from "coder/site/src/api/api"
4- import { createWriteStream } from "fs"
4+ import * as crypto from "crypto"
5+ import { createWriteStream , createReadStream } from "fs"
56import { ensureDir } from "fs-extra"
67import fs from "fs/promises"
78import { IncomingMessage } from "http"
@@ -73,39 +74,16 @@ export class Storage {
7374 // fetchBinary returns the path to a Coder binary.
7475 // The binary will be cached if a matching server version already exists.
7576 public async fetchBinary ( ) : Promise < string | undefined > {
77+ await this . cleanUpOldBinaries ( )
7678 const baseURL = this . getURL ( )
7779 if ( ! baseURL ) {
7880 throw new Error ( "Must be logged in!" )
7981 }
8082 const baseURI = vscode . Uri . parse ( baseURL )
8183
8284 const buildInfo = await getBuildInfo ( )
83- const binPath = this . binaryPath ( buildInfo . version )
84- const exists = await fs
85- . stat ( binPath )
86- . then ( ( ) => true )
87- . catch ( ( ) => false )
88- if ( exists ) {
89- // Even if the file exists, it could be corrupted.
90- // We run `coder version` to ensure the binary can be executed.
91- this . output . appendLine ( `Using cached binary: ${ binPath } ` )
92- const valid = await new Promise < boolean > ( ( resolve ) => {
93- try {
94- execFile ( binPath , [ "version" ] , ( err ) => {
95- if ( err ) {
96- this . output . appendLine ( "Check for binary corruption: " + err )
97- }
98- resolve ( err === null )
99- } )
100- } catch ( ex ) {
101- this . output . appendLine ( "The cached binary cannot be executed: " + ex )
102- resolve ( false )
103- }
104- } )
105- if ( valid ) {
106- return binPath
107- }
108- }
85+ const binPath = this . binaryPath ( )
86+ const exists = await this . checkBinaryExists ( binPath )
10987 const os = goos ( )
11088 const arch = goarch ( )
11189 let binName = `coder-${ os } -${ arch } `
@@ -114,106 +92,153 @@ export class Storage {
11492 binName += ".exe"
11593 }
11694 const controller = new AbortController ( )
95+
96+ if ( exists ) {
97+ this . output . appendLine ( `Found existing binary: ${ binPath } ` )
98+ const valid = await this . checkBinaryValid ( binPath )
99+ if ( ! valid ) {
100+ const removed = await this . rmBinary ( binPath )
101+ if ( ! removed ) {
102+ vscode . window . showErrorMessage ( "Failed to remove existing binary!" )
103+ return undefined
104+ }
105+ }
106+ }
107+ const etag = await this . getBinaryETag ( )
108+ this . output . appendLine ( `Using binName: ${ binName } ` )
109+ this . output . appendLine ( `Using binPath: ${ binPath } ` )
110+ this . output . appendLine ( `Using ETag: ${ etag } ` )
111+
117112 const resp = await axios . get ( "/bin/" + binName , {
118113 signal : controller . signal ,
119114 baseURL : baseURL ,
120115 responseType : "stream" ,
121116 headers : {
122117 "Accept-Encoding" : "gzip" ,
118+ "If-None-Match" : `"${ etag } "` ,
123119 } ,
124120 decompress : true ,
125121 // Ignore all errors so we can catch a 404!
126122 validateStatus : ( ) => true ,
127123 } )
128- if ( resp . status === 404 ) {
129- vscode . window
130- . showErrorMessage (
131- "Coder isn't supported for your platform. Please open an issue, we'd love to support it!" ,
132- "Open an Issue" ,
133- )
134- . then ( ( value ) => {
135- if ( ! value ) {
136- return
137- }
138- const params = new URLSearchParams ( {
139- title : `Support the \`${ os } -${ arch } \` platform` ,
140- body : `I'd like to use the \`${ os } -${ arch } \` architecture with the VS Code extension.` ,
141- } )
142- const uri = vscode . Uri . parse ( `https://github.com/coder/vscode-coder/issues/new?` + params . toString ( ) )
143- vscode . env . openExternal ( uri )
144- } )
145- return
146- }
147- if ( resp . status !== 200 ) {
148- vscode . window . showErrorMessage ( "Failed to fetch the Coder binary: " + resp . statusText )
149- return
150- }
124+ this . output . appendLine ( "Response status code: " + resp . status )
151125
152- const contentLength = Number . parseInt ( resp . headers [ "content-length" ] )
126+ switch ( resp . status ) {
127+ case 200 : {
128+ const contentLength = Number . parseInt ( resp . headers [ "content-length" ] )
153129
154- // Ensure the binary directory exists!
155- await fs . mkdir ( path . dirname ( binPath ) , { recursive : true } )
130+ // Ensure the binary directory exists!
131+ await fs . mkdir ( path . dirname ( binPath ) , { recursive : true } )
132+ const tempFile = binPath + ".temp-" + Math . random ( ) . toString ( 36 ) . substring ( 8 )
156133
157- const completed = await vscode . window . withProgress < boolean > (
158- {
159- location : vscode . ProgressLocation . Notification ,
160- title : `Downloading the latest binary (${ buildInfo . version } from ${ baseURI . authority } )` ,
161- cancellable : true ,
162- } ,
163- async ( progress , token ) => {
164- const readStream = resp . data as IncomingMessage
165- let cancelled = false
166- token . onCancellationRequested ( ( ) => {
167- controller . abort ( )
168- readStream . destroy ( )
169- cancelled = true
170- } )
134+ const completed = await vscode . window . withProgress < boolean > (
135+ {
136+ location : vscode . ProgressLocation . Notification ,
137+ title : `Downloading the latest binary (${ buildInfo . version } from ${ baseURI . authority } )` ,
138+ cancellable : true ,
139+ } ,
140+ async ( progress , token ) => {
141+ const readStream = resp . data as IncomingMessage
142+ let cancelled = false
143+ token . onCancellationRequested ( ( ) => {
144+ controller . abort ( )
145+ readStream . destroy ( )
146+ cancelled = true
147+ } )
171148
172- let contentLengthPretty = ""
173- // Reverse proxies might not always send a content length!
174- if ( ! Number . isNaN ( contentLength ) ) {
175- contentLengthPretty = " / " + prettyBytes ( contentLength )
176- }
149+ let contentLengthPretty = ""
150+ // Reverse proxies might not always send a content length!
151+ if ( ! Number . isNaN ( contentLength ) ) {
152+ contentLengthPretty = " / " + prettyBytes ( contentLength )
153+ }
177154
178- const writeStream = createWriteStream ( binPath , {
179- autoClose : true ,
180- mode : 0o755 ,
181- } )
182- let written = 0
183- readStream . on ( "data" , ( buffer : Buffer ) => {
184- writeStream . write ( buffer , ( ) => {
185- written += buffer . byteLength
186- progress . report ( {
187- message : `${ prettyBytes ( written ) } ${ contentLengthPretty } ` ,
188- increment : ( buffer . byteLength / contentLength ) * 100 ,
155+ const writeStream = createWriteStream ( tempFile , {
156+ autoClose : true ,
157+ mode : 0o755 ,
189158 } )
190- } )
159+ let written = 0
160+ readStream . on ( "data" , ( buffer : Buffer ) => {
161+ writeStream . write ( buffer , ( ) => {
162+ written += buffer . byteLength
163+ progress . report ( {
164+ message : `${ prettyBytes ( written ) } ${ contentLengthPretty } ` ,
165+ increment : ( buffer . byteLength / contentLength ) * 100 ,
166+ } )
167+ } )
168+ } )
169+ try {
170+ await new Promise < void > ( ( resolve , reject ) => {
171+ readStream . on ( "error" , ( err ) => {
172+ reject ( err )
173+ } )
174+ readStream . on ( "close" , ( ) => {
175+ if ( cancelled ) {
176+ return reject ( )
177+ }
178+ writeStream . close ( )
179+ resolve ( )
180+ } )
181+ } )
182+ return true
183+ } catch ( ex ) {
184+ return false
185+ }
186+ } ,
187+ )
188+ if ( ! completed ) {
189+ return
190+ }
191+ this . output . appendLine ( `Downloaded binary: ${ binPath } ` )
192+ const oldBinPath = binPath + ".old-" + Math . random ( ) . toString ( 36 ) . substring ( 8 )
193+ await fs . rename ( binPath , oldBinPath ) . catch ( ( ) => {
194+ this . output . appendLine ( `Warning: failed to rename ${ binPath } to ${ oldBinPath } ` )
191195 } )
192- try {
193- await new Promise < void > ( ( resolve , reject ) => {
194- readStream . on ( "error" , ( err ) => {
195- reject ( err )
196+ await fs . rename ( tempFile , binPath )
197+ await fs . rm ( oldBinPath , { force : true } ) . catch ( ( error ) => {
198+ this . output . appendLine ( `Warning: failed to remove old binary: ${ error } ` )
199+ } )
200+ return binPath
201+ }
202+ case 304 : {
203+ this . output . appendLine ( `Using cached binary: ${ binPath } ` )
204+ return binPath
205+ }
206+ case 404 : {
207+ vscode . window
208+ . showErrorMessage (
209+ "Coder isn't supported for your platform. Please open an issue, we'd love to support it!" ,
210+ "Open an Issue" ,
211+ )
212+ . then ( ( value ) => {
213+ if ( ! value ) {
214+ return
215+ }
216+ const params = new URLSearchParams ( {
217+ title : `Support the \`${ os } -${ arch } \` platform` ,
218+ body : `I'd like to use the \`${ os } -${ arch } \` architecture with the VS Code extension.` ,
196219 } )
197- readStream . on ( "close" , ( ) => {
198- if ( cancelled ) {
199- return reject ( )
200- }
201- writeStream . close ( )
202- resolve ( )
220+ const uri = vscode . Uri . parse ( `https://github.com/coder/vscode-coder/issues/new?` + params . toString ( ) )
221+ vscode . env . openExternal ( uri )
222+ } )
223+ return undefined
224+ }
225+ default : {
226+ vscode . window
227+ . showErrorMessage ( "Failed to download binary. Please open an issue." , "Open an Issue" )
228+ . then ( ( value ) => {
229+ if ( ! value ) {
230+ return
231+ }
232+ const params = new URLSearchParams ( {
233+ title : `Failed to download binary on \`${ os } -${ arch } \`` ,
234+ body : `Received status code \`${ resp . status } \` when downloading the binary.` ,
203235 } )
236+ const uri = vscode . Uri . parse ( `https://github.com/coder/vscode-coder/issues/new?` + params . toString ( ) )
237+ vscode . env . openExternal ( uri )
204238 } )
205- return true
206- } catch ( ex ) {
207- return false
208- }
209- } ,
210- )
211- if ( ! completed ) {
212- return
239+ return undefined
240+ }
213241 }
214-
215- this . output . appendLine ( `Downloaded binary: ${ binPath } ` )
216- return binPath
217242 }
218243
219244 // getBinaryCachePath returns the path where binaries are cached.
@@ -240,6 +265,23 @@ export class Storage {
240265 return path . join ( this . globalStorageUri . fsPath , "url" )
241266 }
242267
268+ public getBinaryETag ( ) : Promise < string > {
269+ const hash = crypto . createHash ( "sha1" )
270+ const stream = createReadStream ( this . binaryPath ( ) )
271+ return new Promise ( ( resolve , reject ) => {
272+ stream . on ( "end" , ( ) => {
273+ hash . end ( )
274+ resolve ( hash . digest ( "hex" ) )
275+ } )
276+ stream . on ( "error" , ( err ) => {
277+ reject ( err )
278+ } )
279+ stream . on ( "data" , ( chunk ) => {
280+ hash . update ( chunk )
281+ } )
282+ } )
283+ }
284+
243285 private appDataDir ( ) : string {
244286 switch ( process . platform ) {
245287 case "darwin" :
@@ -264,16 +306,62 @@ export class Storage {
264306 }
265307 }
266308
267- private binaryPath ( version : string ) : string {
309+ private async cleanUpOldBinaries ( ) : Promise < void > {
310+ const binPath = this . binaryPath ( )
311+ const binDir = path . dirname ( binPath )
312+ const files = await fs . readdir ( binDir )
313+ for ( const file of files ) {
314+ const fileName = path . basename ( file )
315+ if ( fileName . includes ( ".old-" ) ) {
316+ try {
317+ await fs . rm ( path . join ( binDir , file ) , { force : true } )
318+ } catch ( error ) {
319+ this . output . appendLine ( `Warning: failed to remove ${ fileName } . Error: ${ error } ` )
320+ }
321+ }
322+ }
323+ }
324+
325+ private binaryPath ( ) : string {
268326 const os = goos ( )
269327 const arch = goarch ( )
270- let binPath = path . join ( this . getBinaryCachePath ( ) , `coder-${ os } -${ arch } - ${ version } ` )
328+ let binPath = path . join ( this . getBinaryCachePath ( ) , `coder-${ os } -${ arch } ` )
271329 if ( os === "windows" ) {
272330 binPath += ".exe"
273331 }
274332 return binPath
275333 }
276334
335+ private async checkBinaryExists ( binPath : string ) : Promise < boolean > {
336+ return await fs
337+ . stat ( binPath )
338+ . then ( ( ) => true )
339+ . catch ( ( ) => false )
340+ }
341+
342+ private async rmBinary ( binPath : string ) : Promise < boolean > {
343+ return await fs
344+ . rm ( binPath , { force : true } )
345+ . then ( ( ) => true )
346+ . catch ( ( ) => false )
347+ }
348+
349+ private async checkBinaryValid ( binPath : string ) : Promise < boolean > {
350+ return await new Promise < boolean > ( ( resolve ) => {
351+ try {
352+ execFile ( binPath , [ "version" ] , ( err ) => {
353+ if ( err ) {
354+ this . output . appendLine ( "Check for binary corruption: " + err )
355+ }
356+ resolve ( err === null )
357+ } )
358+ } catch ( ex ) {
359+ this . output . appendLine ( "The cached binary cannot be executed: " + ex )
360+ resolve ( false )
361+ }
362+ } )
363+ }
364+
277365 private async updateSessionToken ( ) {
278366 const token = await this . getSessionToken ( )
279367 if ( token ) {
0 commit comments