66import * as fs from 'fs' ;
77import { tmpdir } from 'os' ;
88import { promisify } from 'util' ;
9- import { ResourceQueue } from 'vs/base/common/async' ;
9+ import { ResourceQueue , timeout } from 'vs/base/common/async' ;
1010import { isEqualOrParent , isRootOrDriveLetter , randomPath } from 'vs/base/common/extpath' ;
1111import { normalizeNFC } from 'vs/base/common/normalization' ;
1212import { join } from 'vs/base/common/path' ;
@@ -491,16 +491,24 @@ function ensureWriteOptions(options?: IWriteFileOptions): IEnsuredWriteFileOptio
491491/**
492492 * A drop-in replacement for `fs.rename` that:
493493 * - allows to move across multiple disks
494+ * - attempts to retry the operation for certain error codes on Windows
494495 */
495- async function move ( source : string , target : string ) : Promise < void > {
496+ async function rename ( source : string , target : string , windowsRetryTimeout : number | false = 60000 /* matches graceful-fs */ ) : Promise < void > {
496497 if ( source === target ) {
497498 return ; // simulate node.js behaviour here and do a no-op if paths match
498499 }
499500
500501 try {
501- await Promises . rename ( source , target ) ;
502+ if ( isWindows && typeof windowsRetryTimeout === 'number' ) {
503+ // On Windows, a rename can fail when either source or target
504+ // is locked by AV software. We do leverage graceful-fs to iron
505+ // out these issues, however in case the target file exists,
506+ // graceful-fs will immediately return without retry for fs.rename().
507+ await renameWithRetry ( source , target , Date . now ( ) , windowsRetryTimeout ) ;
508+ } else {
509+ await promisify ( fs . rename ) ( source , target ) ;
510+ }
502511 } catch ( error ) {
503-
504512 // In two cases we fallback to classic copy and delete:
505513 //
506514 // 1.) The EXDEV error indicates that source and target are on different devices
@@ -518,6 +526,44 @@ async function move(source: string, target: string): Promise<void> {
518526 }
519527}
520528
529+ async function renameWithRetry ( source : string , target : string , startTime : number , retryTimeout : number , attempt = 0 ) : Promise < void > {
530+ try {
531+ return await promisify ( fs . rename ) ( source , target ) ;
532+ } catch ( error ) {
533+ if ( error . code !== 'EACCES' && error . code !== 'EPERM' && error . code !== 'EBUSY' ) {
534+ throw error ; // only for errors we think are temporary
535+ }
536+
537+ if ( Date . now ( ) - startTime >= retryTimeout ) {
538+ console . error ( `[node.js fs] rename failed after ${ attempt } retries with error: ${ error } ` ) ;
539+
540+ throw error ; // give up after configurable timeout
541+ }
542+
543+ if ( attempt === 0 ) {
544+ let abortRetry = false ;
545+ try {
546+ const { stat } = await SymlinkSupport . stat ( target ) ;
547+ if ( ! stat . isFile ( ) ) {
548+ abortRetry = true ; // if target is not a file, EPERM error may be raised and we should not attempt to retry
549+ }
550+ } catch ( error ) {
551+ // Ignore
552+ }
553+
554+ if ( abortRetry ) {
555+ throw error ;
556+ }
557+ }
558+
559+ // Delay with incremental backoff up to 100ms
560+ await timeout ( Math . min ( 100 , attempt * 10 ) ) ;
561+
562+ // Attempt again
563+ return renameWithRetry ( source , target , startTime , retryTimeout , attempt + 1 ) ;
564+ }
565+ }
566+
521567interface ICopyPayload {
522568 readonly root : { source : string ; target : string } ;
523569 readonly options : { preserveSymlinks : boolean } ;
@@ -694,7 +740,6 @@ export const Promises = new class {
694740 get fdatasync ( ) { return promisify ( fs . fdatasync ) ; }
695741 get truncate ( ) { return promisify ( fs . truncate ) ; }
696742
697- get rename ( ) { return promisify ( fs . rename ) ; }
698743 get copyFile ( ) { return promisify ( fs . copyFile ) ; }
699744
700745 get open ( ) { return promisify ( fs . open ) ; }
@@ -733,7 +778,7 @@ export const Promises = new class {
733778
734779 get rm ( ) { return rimraf ; }
735780
736- get move ( ) { return move ; }
781+ get rename ( ) { return rename ; }
737782 get copy ( ) { return copy ; }
738783
739784 //#endregion
0 commit comments