Skip to content

Commit 26b56f4

Browse files
committed
Flash pipeline hardening: detach logging, Phase 2 automount, xcopy retry (v3.2.3)
End-to-end audit of the Windows flash pipeline found 10 issues. This fixes the 5 most critical ones: 1. detachWindowsDriveLetters now logs failures instead of silently swallowing them — previously, if mountvol /D and /P both failed, the function returned success and the format command hit the same locked handle on retry 2. Phase 2 format now re-disables automount before formatting — Phase 1 re-enables automount at the end, which lets Explorer grab the raw partition handle before Phase 2 can format it (#49 recurring root cause) 3. xcopy has a 3-attempt retry loop — transient failures from drive stabilizing after format no longer kill the entire flash 4. Preflight drive accessibility check — verifies the drive letter is accessible (5 attempts, 1s each) before starting the copy phase 5. automount re-enable now logs errors instead of silently catching — if re-enable fails, user gets a clear warning about manual fix needed Phase 2 delay also increased from 1500ms to 2000ms to give Windows more time to release partition handles after detach.
1 parent dfa43be commit 26b56f4

2 files changed

Lines changed: 49 additions & 10 deletions

File tree

electron/diskOps.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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');

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "opcore-oneclick",
33
"private": true,
4-
"version": "3.2.2",
4+
"version": "3.2.3",
55
"description": "OpCore-OneClick deployment utility",
66
"author": "OpCore-OneClick contributors",
77
"main": "dist-electron/electron/main.js",

0 commit comments

Comments
 (0)