@@ -564,10 +564,17 @@ export function createDiskOps(log: LogFunction): DiskOps {
564564 // Try soft dismount (/D) first, then hard-remove (/P) for any handle-locked volume.
565565 // /P is safe here: this function is only called immediately before a destructive
566566 // diskpart clean that wipes the entire disk anyway.
567- await runCmd (
568- `powershell -NoProfile -Command "Get-Partition -DiskNumber ${ diskNum } | Where-Object { $_.DriveLetter } | ForEach-Object { $letter = $_.DriveLetter + ':'; try { mountvol $letter /D } catch {}; try { mountvol $letter /P } catch {} }"` ,
569- register ,
570- ) ;
567+ // Also set the partition offline to force Windows to release ALL handles (#49).
568+ try {
569+ await runCmd (
570+ `powershell -NoProfile -Command "Get-Partition -DiskNumber ${ diskNum } -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { $letter = $_.DriveLetter + ':'; try { mountvol $letter /D } catch {}; try { mountvol $letter /P } catch {}; Write-Output ('Detached ' + $letter) }"` ,
571+ register ,
572+ ) ;
573+ } catch ( e : any ) {
574+ log ( 'WARN' , 'diskOps' , 'detachWindowsDriveLetters failed — volume handles may persist' , {
575+ diskNum, error : e ?. message ,
576+ } ) ;
577+ }
571578 }
572579
573580 async function assignWindowsDriveLetter (
@@ -1378,10 +1385,12 @@ export function createDiskOps(log: LogFunction): DiskOps {
13781385 diskNum, partNum, attempt : attempt + 1 ,
13791386 } ) ;
13801387
1388+ // Re-disable automount before Phase 2 — it was re-enabled at the end of Phase 1.
1389+ // Without this, Explorer grabs the raw partition handle between Phase 1 and Phase 2 (#49).
1390+ await runCmd ( 'powershell -NoProfile -Command "\'automount disable\' | diskpart | Out-Null"' , registerProcess ) . catch ( ( ) => { } ) ;
13811391 // Clear handle locks: Explorer/Shell auto-mounts new partitions immediately.
1382- // Even with automount disabled, some Windows builds still briefly grab handles.
13831392 await detachWindowsDriveLetters ( diskNum , registerProcess ) . catch ( ( ) => { } ) ;
1384- await new Promise ( ( resolve ) => setTimeout ( resolve , 1500 ) ) ;
1393+ await new Promise ( ( resolve ) => setTimeout ( resolve , 2000 ) ) ;
13851394
13861395 // Phase 2 diskpart: format WITHOUT noerr
13871396 const phase2Script = buildWindowsFormatDiskpartScript ( diskNum , partNum ) ;
@@ -1483,7 +1492,9 @@ export function createDiskOps(log: LogFunction): DiskOps {
14831492 let formatRecovered = false ;
14841493 try {
14851494 // Re-enable automount before Format-Volume (may have been disabled in Phase 1)
1486- await runCmd ( 'powershell -NoProfile -Command "\'automount enable\' | diskpart | Out-Null"' , registerProcess ) . catch ( ( ) => { } ) ;
1495+ await runCmd ( 'powershell -NoProfile -Command "\'automount enable\' | diskpart | Out-Null"' , registerProcess ) . catch ( ( e : any ) => {
1496+ log ( 'ERROR' , 'diskOps' , 'Failed to re-enable automount — run "automount enable" in diskpart manually' , { error : e ?. message } ) ;
1497+ } ) ;
14871498 await runCmd (
14881499 `powershell -NoProfile -Command "try { Format-Volume -DiskNumber ${ diskNum } -PartitionNumber ${ partNum } -FileSystem FAT32 -NewFileSystemLabel 'OPENCORE' -Confirm:$false -Force -ErrorAction Stop } catch { throw }"` ,
14891500 registerProcess ,
@@ -1517,7 +1528,9 @@ export function createDiskOps(log: LogFunction): DiskOps {
15171528 }
15181529 if ( formatRecovered && driveLetter ) return { diskNum, driveLetter } ;
15191530 // Re-enable automount before throwing so Windows returns to normal state
1520- await runCmd ( 'powershell -NoProfile -Command "\'automount enable\' | diskpart | Out-Null"' , registerProcess ) . catch ( ( ) => { } ) ;
1531+ await runCmd ( 'powershell -NoProfile -Command "\'automount enable\' | diskpart | Out-Null"' , registerProcess ) . catch ( ( e : any ) => {
1532+ log ( 'ERROR' , 'diskOps' , 'Failed to re-enable automount — run "automount enable" in diskpart manually' , { error : e ?. message } ) ;
1533+ } ) ;
15211534 throw new Error (
15221535 `Disk format failed: Windows could not format the partition as FAT32. ` +
15231536 'Another process may be locking the drive, or the drive controller is rejecting the format command. ' +
@@ -1801,10 +1814,36 @@ export function createDiskOps(log: LogFunction): DiskOps {
18011814 ) ;
18021815
18031816 try {
1817+ // Preflight: verify drive letter is accessible before attempting copy
1818+ const driveRoot = `${ driveLetter } :\\` ;
1819+ for ( let driveCheck = 0 ; driveCheck < 5 ; driveCheck ++ ) {
1820+ if ( fs . existsSync ( driveRoot ) ) break ;
1821+ log ( 'WARN' , 'usb-flash' , `Drive ${ driveLetter } : not yet accessible, waiting...` , { attempt : driveCheck + 1 } ) ;
1822+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
1823+ }
1824+ if ( ! fs . existsSync ( driveRoot ) ) {
1825+ throw new Error ( `Drive ${ driveLetter } : is not accessible after format. The drive may have been ejected or the format did not complete.` ) ;
1826+ }
1827+
18041828 onPhase ( 'copy' , `Copying EFI to ${ driveLetter } :` ) ;
18051829 checkAborted ( ) ;
18061830 log ( 'DEBUG' , 'usb-flash' , 'Copying EFI' , { driveLetter } ) ;
1807- await runCmd ( `xcopy /E /I /H /Y "${ path . join ( efiPath , 'EFI' ) } " "${ driveLetter } :\\EFI"` , registerProcess ) ;
1831+ // Retry xcopy up to 3 times — transient failures from drive stabilizing after format
1832+ let copyAttempt = 0 ;
1833+ const maxCopyAttempts = 3 ;
1834+ while ( true ) {
1835+ try {
1836+ await runCmd ( `xcopy /E /I /H /Y "${ path . join ( efiPath , 'EFI' ) } " "${ driveLetter } :\\EFI"` , registerProcess ) ;
1837+ break ;
1838+ } catch ( copyErr ) {
1839+ copyAttempt ++ ;
1840+ if ( copyAttempt >= maxCopyAttempts ) throw copyErr ;
1841+ log ( 'WARN' , 'usb-flash' , `EFI copy failed (attempt ${ copyAttempt } /${ maxCopyAttempts } ), retrying...` , {
1842+ driveLetter, error : ( copyErr as Error ) . message ,
1843+ } ) ;
1844+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1500 ) ) ;
1845+ }
1846+ }
18081847
18091848 // Copy recovery payload if present — non-fatal so EFI flash still succeeds
18101849 const recoveryDir = path . join ( efiPath , 'com.apple.recovery.boot' ) ;
0 commit comments