Skip to content

Commit 23c8d79

Browse files
committed
fix: production-ready USB formatting, silent background processes, and forced UAC elevation on Windows
1 parent 617660f commit 23c8d79

8 files changed

Lines changed: 161 additions & 97 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: "Release"
22

33
on:
44
push:
5-
branches:
6-
- main
75
tags:
86
- "v*"
97
# Allows you to run this workflow manually from the Actions tab
@@ -40,10 +38,10 @@ jobs:
4038
with:
4139
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
4240

43-
- name: Setup Node
41+
- name: setup node
4442
uses: actions/setup-node@v4
4543
with:
46-
node-version: 20
44+
node-version: 24
4745
cache: "npm"
4846

4947
- name: Install dependencies (Ubuntu only)
@@ -59,6 +57,7 @@ jobs:
5957
uses: tauri-apps/tauri-action@v0
6058
env:
6159
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
6261
with:
6362
tagName: v__VERSION__ # The action automatically replaces \_\_VERSION\_\_ with the app version
6463
releaseName: "BootISO v__VERSION__"

index.html

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
<!doctype html>
22
<html lang="en">
3-
<head>
4-
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<title>BootISO — USB Flasher</title>
8-
<meta name="description" content="Cross-platform ISO to USB flasher. Flash Windows, Linux and other ISOs to USB drives with ease." />
9-
</head>
10-
<body>
11-
<div id="root"></div>
12-
<script type="module" src="/src/main.tsx"></script>
13-
</body>
14-
</html>
3+
4+
<head>
5+
<meta charset="UTF-8" />
6+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>BootISO - USB Flasher</title>
9+
<meta name="description"
10+
content="Cross-platform ISO to USB flasher. Flash Windows, Linux and other ISOs to USB drives with ease." />
11+
</head>
12+
13+
<body>
14+
<div id="root"></div>
15+
<script type="module" src="/src/main.tsx"></script>
16+
</body>
17+
18+
</html>

src-tauri/build.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
11
fn main() {
2-
tauri_build::build()
2+
let mut windows = tauri_build::WindowsAttributes::new();
3+
windows = windows.app_manifest(
4+
r#"
5+
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
6+
<dependency>
7+
<dependentAssembly>
8+
<assemblyIdentity
9+
type="win32"
10+
name="Microsoft.Windows.Common-Controls"
11+
version="6.0.0.0"
12+
processorArchitecture="*"
13+
publicKeyToken="6595b64144ccf1df"
14+
language="*"
15+
/>
16+
</dependentAssembly>
17+
</dependency>
18+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
19+
<security>
20+
<requestedPrivileges>
21+
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
22+
</requestedPrivileges>
23+
</security>
24+
</trustInfo>
25+
</assembly>
26+
"#
27+
);
28+
29+
tauri_build::try_build(
30+
tauri_build::Attributes::new().windows_attributes(windows)
31+
).expect("failed to build tauri build script");
332
}

src-tauri/src/commands.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ pub struct PlatformInfo {
5959
#[cfg(target_os = "windows")]
6060
fn check_admin_status() -> bool {
6161
use std::process::Command;
62+
use std::os::windows::process::CommandExt;
6263
Command::new("net")
6364
.args(["session"])
65+
.creation_flags(0x08000000)
6466
.output()
6567
.map(|o| o.status.success())
6668
.unwrap_or(false)

src-tauri/src/usb.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ fn format_size(bytes: u64) -> String {
3333

3434
#[cfg(target_os = "windows")]
3535
pub fn list_usb_devices() -> Result<Vec<UsbDevice>, String> {
36+
use std::os::windows::process::CommandExt;
37+
3638
// Use PowerShell to get removable drives via WMI
3739
let output = Command::new("powershell")
3840
.args([
@@ -56,6 +58,7 @@ pub fn list_usb_devices() -> Result<Vec<UsbDevice>, String> {
5658
}
5759
"#,
5860
])
61+
.creation_flags(0x08000000)
5962
.output()
6063
.map_err(|e| format!("Failed to execute PowerShell: {}", e))?;
6164

src-tauri/src/windows_iso.rs

Lines changed: 102 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
use crate::writer::{FlashOptions, FlashProgress, FlashResult};
22
#[cfg(target_os = "windows")]
33
use std::process::{Command, Stdio};
4+
#[cfg(target_os = "windows")]
5+
use std::os::windows::process::CommandExt;
46
use std::sync::atomic::{AtomicBool, Ordering};
57
use std::time::Instant;
68
use tauri::AppHandle;
79
use tauri::Emitter;
810

11+
// CREATE_NO_WINDOW: prevents PowerShell/diskpart windows from popping up
12+
#[cfg(target_os = "windows")]
13+
const CREATE_NO_WINDOW: u32 = 0x08000000;
14+
915
static CANCEL_FLAG: AtomicBool = AtomicBool::new(false);
1016

1117
pub fn cancel_flash() {
@@ -163,6 +169,7 @@ fn mount_iso(iso_path: &str) -> Result<String, String> {
163169

164170
let output = Command::new("powershell")
165171
.args(["-NoProfile", "-Command", &script])
172+
.creation_flags(CREATE_NO_WINDOW)
166173
.output()
167174
.map_err(|e| format!("Failed to execute Mount PowerShell: {}", e))?;
168175

@@ -186,6 +193,7 @@ fn unmount_iso(iso_path: &str) -> Result<(), String> {
186193
let script = format!(r#"Dismount-DiskImage -ImagePath "{}""#, iso_path);
187194
Command::new("powershell")
188195
.args(["-NoProfile", "-Command", &script])
196+
.creation_flags(CREATE_NO_WINDOW)
189197
.output()
190198
.map_err(|e| format!("Failed to dismount ISO: {}", e))?;
191199
Ok(())
@@ -207,113 +215,125 @@ fn format_usb_drive(disk_number: u32, options: &FlashOptions) -> Result<(String,
207215
let is_gpt = options.partition_scheme.as_deref().unwrap_or("mbr").eq_ignore_ascii_case("gpt");
208216
let label = options.volume_label.as_deref().unwrap_or("BOOTISO");
209217

218+
// Create a temporary file for the diskpart script
219+
let temp_dir = std::env::temp_dir();
220+
let script_path = temp_dir.join(format!("bootiso_diskpart_{}.txt", disk_number));
221+
210222
if is_gpt {
211-
// ===== GPT: Use PowerShell for reliable GPT partitioning =====
212-
let ps_script = format!(
213-
r#"
214-
$ErrorActionPreference = 'Stop'
215-
# 1. Clean and initialize disk as GPT
216-
Clear-Disk -Number {disk} -RemoveData -RemoveOEM -Confirm:$false -ErrorAction SilentlyContinue
217-
Initialize-Disk -Number {disk} -PartitionStyle GPT -Confirm:$false -ErrorAction SilentlyContinue
218-
219-
# 2. Create main partition (leave 1MB at end for UEFI:NTFS)
220-
$mainPart = New-Partition -DiskNumber {disk} -UseMaximumSize -AssignDriveLetter
221-
222-
while (!(Get-Volume -DriveLetter $mainPart.DriveLetter -ErrorAction SilentlyContinue)) {{
223-
Start-Sleep -Milliseconds 500
224-
}}
225-
Format-Volume -Partition $mainPart -FileSystem {fs} -NewFileSystemLabel "{label}" -Confirm:$false | Out-Null
226-
227-
# 3. Shrink main partition by 1MB to make room for UEFI:NTFS
228-
$newSize = $mainPart.Size - 1MB
229-
Resize-Partition -DiskNumber {disk} -PartitionNumber $mainPart.PartitionNumber -Size $newSize
230-
231-
# 4. Create small FAT partition in the freed space
232-
$fatPart = New-Partition -DiskNumber {disk} -UseMaximumSize -AssignDriveLetter
233-
234-
# 5. Wait for the volume to be available before formatting to avoid race conditions
235-
while (!(Get-Volume -DriveLetter $fatPart.DriveLetter -ErrorAction SilentlyContinue)) {{
236-
Start-Sleep -Milliseconds 500
237-
}}
238-
Format-Volume -Partition $fatPart -FileSystem FAT -NewFileSystemLabel "UEFI_NTFS" -Confirm:$false | Out-Null
239-
240-
# 6. Output both drive letters (main first, fat second)
241-
Write-Output $mainPart.DriveLetter
242-
Write-Output $fatPart.DriveLetter
243-
"#,
244-
disk = disk_number,
245-
fs = fs_cmd,
246-
label = label
223+
let diskpart_script = format!(
224+
"select disk {}\nclean\nconvert gpt\ncreate partition primary\nshrink desired=2 minimum=2\nformat fs={} label=\"{}\" quick\nassign\ncreate partition primary\nformat fs=fat label=\"UEFI_NTFS\" quick\nassign\nexit\n",
225+
disk_number, fs_cmd, label
247226
);
248227

249-
let output = Command::new("powershell")
250-
.args(["-NoProfile", "-Command", &ps_script])
228+
std::fs::write(&script_path, diskpart_script)
229+
.map_err(|e| format!("Failed to write diskpart script: {}", e))?;
230+
231+
let output = Command::new("diskpart")
232+
.args(["/s", script_path.to_str().unwrap()])
233+
.creation_flags(CREATE_NO_WINDOW)
251234
.output()
252-
.map_err(|e| format!("Failed to execute PowerShell GPT format: {}", e))?;
235+
.map_err(|e| format!("Failed to execute diskpart (GPT): {}", e))?;
236+
237+
let _ = std::fs::remove_file(&script_path);
253238

254239
if !output.status.success() {
255240
return Err(format!(
256-
"Failed to format USB drive (GPT) via PowerShell: {}",
257-
String::from_utf8_lossy(&output.stderr)
241+
"Diskpart GPT format failed: {}",
242+
String::from_utf8_lossy(&output.stdout) // diskpart usually outputs errors to stdout
258243
));
259244
}
260245

261-
let stdout = String::from_utf8_lossy(&output.stdout);
262-
let lines: Vec<&str> = stdout.trim().lines().collect();
263-
if lines.len() >= 2 {
264-
let main_letter = lines[lines.len() - 2].trim().to_string();
265-
let fat_letter = lines[lines.len() - 1].trim().to_string();
246+
// Retrieve the two assigned drive letters using PowerShell since diskpart output parsing is frail
247+
// We look for volumes on this specific disk
248+
let get_letters_script = format!(
249+
r#"
250+
$letters = Get-Partition -DiskNumber {} | Where-Object DriveLetter | Select-Object -ExpandProperty DriveLetter
251+
$letters -join "`n"
252+
"#,
253+
disk_number
254+
);
255+
256+
let letters_output = Command::new("powershell")
257+
.args(["-NoProfile", "-Command", &get_letters_script])
258+
.creation_flags(CREATE_NO_WINDOW)
259+
.output()
260+
.map_err(|e| format!("Failed to get drive letters: {}", e))?;
261+
262+
let letters_str = String::from_utf8_lossy(&letters_output.stdout);
263+
let mut letters: Vec<&str> = letters_str.trim().lines().collect();
264+
letters.sort(); // Main partition is usually larger/first, but let's assume alphabetical if we must, or we can check labels.
265+
266+
// Better: get letters by Label to be 100% sure
267+
let get_letters_by_label = format!(
268+
r#"
269+
$main = Get-Partition -DiskNumber {0} | Get-Volume | Where-Object FileSystemLabel -eq '{1}' | Select-Object -ExpandProperty DriveLetter
270+
$fat = Get-Partition -DiskNumber {0} | Get-Volume | Where-Object FileSystemLabel -eq 'UEFI_NTFS' | Select-Object -ExpandProperty DriveLetter
271+
Write-Output $main
272+
Write-Output $fat
273+
"#,
274+
disk_number, label
275+
);
276+
277+
let precise_output = Command::new("powershell")
278+
.args(["-NoProfile", "-Command", &get_letters_by_label])
279+
.creation_flags(CREATE_NO_WINDOW)
280+
.output()
281+
.map_err(|e| format!("Failed to get precise drive letters: {}", e))?;
282+
283+
let precise_str = String::from_utf8_lossy(&precise_output.stdout);
284+
let precise_lines: Vec<&str> = precise_str.trim().lines().collect();
285+
286+
if precise_lines.len() >= 2 {
287+
let main_letter = precise_lines[0].trim().to_string();
288+
let fat_letter = precise_lines[1].trim().to_string();
266289
Ok((format!("{}:\\", main_letter), Some(format!("{}:\\", fat_letter))))
267290
} else {
268291
Err(format!(
269-
"GPT format succeeded but could not find two drive letters. Output: {}",
270-
stdout
292+
"GPT format succeeded but could not verify both partition letters via WMI. Output: {}",
293+
precise_str
271294
))
272295
}
296+
273297
} else {
274-
// ===== MBR: Use PowerShell (more reliable than diskpart) =====
275-
let ps_script = format!(
276-
r#"
277-
$ErrorActionPreference = 'Stop'
278-
# 1. Clean disk
279-
Clear-Disk -Number {disk} -RemoveData -RemoveOEM -Confirm:$false -ErrorAction SilentlyContinue
280-
281-
# 2. Force MBR partition style
282-
Set-Disk -Number {disk} -PartitionStyle MBR -ErrorAction SilentlyContinue
283-
Initialize-Disk -Number {disk} -PartitionStyle MBR -ErrorAction SilentlyContinue
284-
285-
# 3. Create main partition and make it active
286-
$mainPart = New-Partition -DiskNumber {disk} -UseMaximumSize -IsActive -AssignDriveLetter
287-
Format-Volume -Partition $mainPart -FileSystem {fs} -NewFileSystemLabel "{label}" -Confirm:$false | Out-Null
288-
289-
# 4. Output drive letter
290-
Write-Output $mainPart.DriveLetter
291-
"#,
292-
disk = disk_number,
293-
fs = fs_cmd,
294-
label = label
298+
// ===== MBR: Use diskpart =====
299+
let diskpart_script = format!(
300+
"select disk {}\nclean\nconvert mbr\ncreate partition primary\nactive\nformat fs={} label=\"{}\" quick\nassign\nexit\n",
301+
disk_number, fs_cmd, label
295302
);
296303

297-
let output = Command::new("powershell")
298-
.args(["-NoProfile", "-Command", &ps_script])
304+
std::fs::write(&script_path, diskpart_script)
305+
.map_err(|e| format!("Failed to write diskpart script: {}", e))?;
306+
307+
let output = Command::new("diskpart")
308+
.args(["/s", script_path.to_str().unwrap()])
309+
.creation_flags(CREATE_NO_WINDOW)
299310
.output()
300-
.map_err(|e| format!("Failed to execute PowerShell MBR format: {}", e))?;
311+
.map_err(|e| format!("Failed to execute diskpart (MBR): {}", e))?;
312+
313+
let _ = std::fs::remove_file(&script_path);
301314

302315
if !output.status.success() {
303316
return Err(format!(
304-
"Failed to format USB drive (MBR) via PowerShell: {}",
305-
String::from_utf8_lossy(&output.stderr)
317+
"Diskpart MBR format failed: {}",
318+
String::from_utf8_lossy(&output.stdout)
306319
));
307320
}
308321

309-
let stdout = String::from_utf8_lossy(&output.stdout);
310-
let letter = stdout.trim().to_string();
322+
let get_letter_script = format!(
323+
"(Get-Partition -DiskNumber {} | Where-Object DriveLetter | Select-Object -First 1).DriveLetter",
324+
disk_number
325+
);
326+
327+
let letter_output = Command::new("powershell")
328+
.args(["-NoProfile", "-Command", &get_letter_script])
329+
.creation_flags(CREATE_NO_WINDOW)
330+
.output()
331+
.map_err(|e| format!("Failed to get MBR drive letter: {}", e))?;
332+
333+
let letter = String::from_utf8_lossy(&letter_output.stdout).trim().to_string();
311334

312335
if letter.is_empty() {
313-
return Err(format!(
314-
"MBR format succeeded but could not find drive letter. Output: {}",
315-
stdout
316-
));
336+
return Err("MBR format succeeded but could not find drive letter.".to_string());
317337
}
318338

319339
Ok((format!("{}:\\", letter), None))
@@ -435,6 +455,7 @@ fn copy_files_robocopy(app: &AppHandle, source: &str, dest: &str, total_bytes: u
435455
// We use /E instead of /MIR. We removed /NP to allow percentage tracking.
436456
let mut child = Command::new("robocopy")
437457
.args([source, dest, "/E", "/MT:16", "/J", "/R:0", "/W:0", "/BYTES", "/NDL", "/NJH", "/NJS", "/FP"])
458+
.creation_flags(CREATE_NO_WINDOW)
438459
.stdout(Stdio::piped())
439460
.spawn()
440461
.map_err(|e| format!("Failed to start robocopy: {}", e))?;
@@ -569,6 +590,7 @@ fn install_bootloader(iso_letter: &str, usb_letter: &str) -> Result<(), String>
569590

570591
let output = Command::new(&bootsect_path)
571592
.args(["/nt60", &target, "/force", "/mbr"])
593+
.creation_flags(CREATE_NO_WINDOW)
572594
.output();
573595

574596
match output {

src-tauri/src/writer.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ fn format_iso_size(bytes: u64) -> String {
373373
#[cfg(target_os = "windows")]
374374
fn open_device_for_writing(device_path: &str) -> Result<File, String> {
375375
use std::os::windows::fs::OpenOptionsExt;
376+
use std::os::windows::process::CommandExt;
376377

377378
// Use PowerShell to clear the disk first to prevent Windows "Access Denied" (os error 5)
378379
// when writing raw bytes to a disk that has mounted volumes.
@@ -389,6 +390,7 @@ fn open_device_for_writing(device_path: &str) -> Result<File, String> {
389390
);
390391
let _ = std::process::Command::new("powershell")
391392
.args(["-NoProfile", "-Command", &script])
393+
.creation_flags(0x08000000) // CREATE_NO_WINDOW
392394
.output();
393395
// Give Windows a moment to unmount everything
394396
std::thread::sleep(std::time::Duration::from_millis(1500));

0 commit comments

Comments
 (0)