@@ -203,12 +203,52 @@ export function trampolineBinary() {
203203 return _trampoline
204204}
205205
206+ const downloadsInProgress = new Map < string , Promise < void > > ( )
207+
206208export async function downloadBinary ( bin : DownloadableBinary ) {
207209 const isDownloaded = await fileExists ( bin . path )
208210 if ( isDownloaded ) {
209211 return
210212 }
211213
214+ // Downloads cannot run concurrently with `exec` since the `moveFile`
215+ // operation is not atomic. It will delete the destination file if it exists
216+ // which will cause `exec` to break if it tries to execute the file before
217+ // the source file has been moved.
218+ // We prevent downloads from happening concurrently with `exec` by enforcing
219+ // that only one download happens for an executable. If we get here, we check
220+ // if a download is in progress, wait for it to finish, and return without
221+ // downloading again. If it's not in progress, then we start the download.
222+ const downloadPromise = downloadsInProgress . get ( bin . path )
223+ if ( downloadPromise ) {
224+ await downloadPromise
225+ // Return now since we can assume it's downloaded. If it's not, an exception should've
226+ // been thrown before which will cause the user-level operation to fail anyway.
227+ return
228+ }
229+
230+ // Do not perform any `await`s until we've called `downloadsInProgress.set`.
231+ // Calling `await` before that can cause a different JS task to become active
232+ // and start a concurrent download for this binary.
233+
234+ // My mental model is `performDownload` without the `await` will run
235+ // synchronously until the first `await` in the function and then
236+ // immediately return the promise which we then immediately store. Since JS
237+ // doesn't have preemptive concurrency, we should be able to safely assume a
238+ // different task in the task queue will not run in between starting
239+ // `downloadFn` and the `set` operation on the following line.
240+ const downloadOp = performDownload ( bin )
241+ downloadsInProgress . set ( bin . path , downloadOp )
242+ // Ensure we clean the entry if there's a failure.
243+ try {
244+ // Wait for the download to finish
245+ await downloadOp
246+ } finally {
247+ downloadsInProgress . delete ( bin . path )
248+ }
249+ }
250+
251+ async function performDownload ( bin : DownloadableBinary ) {
212252 const url = bin . downloadUrl ( process . platform , process . arch )
213253 outputDebug ( `Downloading ${ bin . name } ${ bin . version } from ${ url } to ${ bin . path } ` )
214254 const dir = dirname ( bin . path )
0 commit comments