11import axios from "axios"
22import { execFile } from "child_process"
33import { getBuildInfo } from "coder/site/src/api/api"
4+ import * as crypto from "crypto"
45import { createWriteStream } from "fs"
56import { ensureDir } from "fs-extra"
67import fs from "fs/promises"
@@ -81,31 +82,7 @@ export class Storage {
8182
8283 const buildInfo = await getBuildInfo ( )
8384 const binPath = this . binaryPath ( )
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 exists = await this . checkBinaryExists ( binPath )
10986 const os = goos ( )
11087 const arch = goarch ( )
11188 let binName = `coder-${ os } -${ arch } `
@@ -114,6 +91,23 @@ export class Storage {
11491 binName += ".exe"
11592 }
11693 const controller = new AbortController ( )
94+
95+ if ( exists ) {
96+ this . output . appendLine ( `Checking if binary outdated...` )
97+ const outdated = await this . checkBinaryOutdated ( binName , baseURL , controller )
98+ // If it's outdated, we fall through to the download logic.
99+ if ( outdated ) {
100+ this . output . appendLine ( `Found outdated version.` )
101+ } else {
102+ // Even if the file exists, it could be corrupted.
103+ // We run `coder version` to ensure the binary can be executed.
104+ this . output . appendLine ( `Using existing binary: ${ binPath } ` )
105+ const valid = await this . checkBinaryValid ( binPath )
106+ if ( valid ) {
107+ return binPath
108+ }
109+ }
110+ }
117111 const resp = await axios . get ( "/bin/" + binName , {
118112 signal : controller . signal ,
119113 baseURL : baseURL ,
@@ -236,6 +230,10 @@ export class Storage {
236230 return path . join ( this . globalStorageUri . fsPath , "url" )
237231 }
238232
233+ public getBinaryETag ( ) : string {
234+ return crypto . createHash ( "sha1" ) . update ( this . binaryPath ( ) ) . digest ( "hex" )
235+ }
236+
239237 private appDataDir ( ) : string {
240238 switch ( process . platform ) {
241239 case "darwin" :
@@ -270,6 +268,47 @@ export class Storage {
270268 return binPath
271269 }
272270
271+ private async checkBinaryExists ( binPath : string ) : Promise < boolean > {
272+ return await fs
273+ . stat ( binPath )
274+ . then ( ( ) => true )
275+ . catch ( ( ) => false )
276+ }
277+
278+ private async checkBinaryValid ( binPath : string ) : Promise < boolean > {
279+ return await new Promise < boolean > ( ( resolve ) => {
280+ try {
281+ execFile ( binPath , [ "version" ] , ( err ) => {
282+ if ( err ) {
283+ this . output . appendLine ( "Check for binary corruption: " + err )
284+ }
285+ resolve ( err === null )
286+ } )
287+ } catch ( ex ) {
288+ this . output . appendLine ( "The cached binary cannot be executed: " + ex )
289+ resolve ( false )
290+ }
291+ } )
292+ }
293+
294+ private async checkBinaryOutdated ( binName : string , baseURL : string , controller : AbortController ) : Promise < boolean > {
295+ const resp = await axios . get ( "/bin/" + binName , {
296+ signal : controller . signal ,
297+ baseURL : baseURL ,
298+ headers : {
299+ "If-None-Match" : this . getBinaryETag ( ) ,
300+ } ,
301+ } )
302+
303+ switch ( resp . status ) {
304+ case 200 :
305+ return true
306+ case 304 :
307+ default :
308+ return false
309+ }
310+ }
311+
273312 private async updateSessionToken ( ) {
274313 const token = await this . getSessionToken ( )
275314 if ( token ) {
0 commit comments