Technician tool v0.6
Imported from the Google Drive folder 004-technician_tool/v0.6.
This commit is contained in:
1 parent
f5b32e1554
commit
22bd853ecf
30 files changed
+1521
-568
No files matched your search
+49
-62
@@ -3,11 +3,12 @@
|
||||
Card format required by the firmware: MBR partition table, FAT32 (its FAT
|
||||
driver has no exFAT and no long file names), ENV.TXT in the root.
|
||||
|
||||
Windows: formatting needs administrator rights, so a generated PowerShell
|
||||
script runs elevated (UAC prompt). The script re-checks the disk (number,
|
||||
size, bus, not system/boot) right before erasing it, so a card swapped
|
||||
after the technician confirmed is not touched. Windows cannot format FAT32
|
||||
above 32 GB, so larger cards get a 32 GB partition (plenty for the box).
|
||||
Windows: formatting needs administrator rights, so kota_tech.sd.win_format
|
||||
runs elevated (UAC prompt). It re-checks the disk (number, size, bus, not the
|
||||
Windows drive) right before erasing it, so a card swapped after the
|
||||
technician confirmed is not touched, and it writes MBR + FAT32 itself
|
||||
(no Storage cmdlets, which stalled on empty card-reader slots). Larger cards
|
||||
get a 32 GB FAT32 partition (Windows convention; plenty for the box).
|
||||
macOS: `diskutil eraseDisk FAT32 KOTA MBRFormat` (same as format_sd.sh).
|
||||
"""
|
||||
|
||||
@@ -23,11 +24,10 @@ import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .disks import CARD_BUSES, Disk
|
||||
from .disks import Disk
|
||||
from .envfile import BUFFER_NAME, ENV_NAME, EnvCheck, EnvConfig, find_case_insensitive, parse_env
|
||||
|
||||
VOLUME_LABEL = "KOTA"
|
||||
FAT32_MAX = 32 * 1024**3
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -49,31 +49,6 @@ class CardReport:
|
||||
|
||||
# ----------------------------------------------------------------- Windows
|
||||
|
||||
def windows_format_script(disk: Disk, result_file: Path) -> str:
|
||||
buses = ",".join(f"'{b}'" for b in sorted(CARD_BUSES))
|
||||
return f"""
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$result = @{{ ok = $false; message = ''; drive = '' }}
|
||||
try {{
|
||||
$n = {int(disk.id)}
|
||||
$d = Get-Disk -Number $n
|
||||
if ($d.IsSystem -or $d.IsBoot) {{ throw 'Refusing: this is a system/boot disk.' }}
|
||||
if (@({buses}) -notcontains [string]$d.BusType) {{ throw "Refusing: disk bus is $($d.BusType), not an SD/USB card." }}
|
||||
if ([int64]$d.Size -ne {int(disk.size)}) {{ throw 'Refusing: the disk changed since it was selected (card swapped?).' }}
|
||||
if ($d.PartitionStyle -ne 'RAW') {{ Clear-Disk -Number $n -RemoveData -RemoveOEM -Confirm:$false }}
|
||||
Initialize-Disk -Number $n -PartitionStyle MBR -ErrorAction SilentlyContinue
|
||||
$size = [Math]::Min([int64]$d.Size - 2MB, [int64]{FAT32_MAX})
|
||||
$p = New-Partition -DiskNumber $n -Size $size -MbrType FAT32 -AssignDriveLetter
|
||||
Format-Volume -Partition $p -FileSystem FAT32 -NewFileSystemLabel '{VOLUME_LABEL}' -Confirm:$false -Force | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
$letter = (Get-Partition -DiskNumber $n -PartitionNumber $p.PartitionNumber).DriveLetter
|
||||
if (-not $letter) {{ throw 'Formatted, but Windows gave the card no drive letter.' }}
|
||||
$result.ok = $true; $result.drive = "$($letter):\\"; $result.message = 'Formatted FAT32 (MBR).'
|
||||
}} catch {{ $result.message = $_.Exception.Message }}
|
||||
$result | ConvertTo-Json | Out-File -FilePath '{result_file}' -Encoding utf8
|
||||
"""
|
||||
|
||||
|
||||
WORK_ROOT = Path.home() / "KotaTechnicianTool" / "tmp"
|
||||
|
||||
|
||||
@@ -86,42 +61,54 @@ def _workdir() -> Path:
|
||||
return work
|
||||
|
||||
|
||||
def _query_windows_card(number: int) -> tuple[str, str, str]:
|
||||
"""(drive, filesystem, label) of the card's first partition - no admin rights needed."""
|
||||
ps = (f"$p = Get-Partition -DiskNumber {number} -ErrorAction SilentlyContinue | Select-Object -First 1; "
|
||||
"if ($p) { $v = $p | Get-Volume -ErrorAction SilentlyContinue; "
|
||||
"\"$($p.DriveLetter)|$($v.FileSystem)|$($v.FileSystemLabel)\" }")
|
||||
out = subprocess.run(["powershell", "-NoProfile", "-Command", ps], capture_output=True, text=True,
|
||||
timeout=90, creationflags=subprocess.CREATE_NO_WINDOW).stdout.strip()
|
||||
drive, fs, label = (out.split("|") + ["", "", ""])[:3]
|
||||
return (f"{drive}:\\" if drive.strip() else ""), fs, label
|
||||
def win_format_command(disk: Disk, result: Path) -> tuple[str, str, str]:
|
||||
"""(program, parameters, working dir) for the elevated helper."""
|
||||
root = Path(__file__).resolve().parents[2] # the version folder holding kota_tech/
|
||||
params = f'-m kota_tech.sd.win_format --disk {int(disk.id)} --size {int(disk.size)} ' \
|
||||
f'--label {VOLUME_LABEL} --result "{result}"'
|
||||
return sys.executable, params, str(root)
|
||||
|
||||
|
||||
def _run_elevated(program: str, params: str, cwd: str, timeout_s: int = 300) -> int | None:
|
||||
"""ShellExecuteEx 'runas' + wait. None if the technician refused the UAC prompt."""
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class SHELLEXECUTEINFOW(ctypes.Structure):
|
||||
_fields_ = [("cbSize", wintypes.DWORD), ("fMask", ctypes.c_ulong), ("hwnd", wintypes.HWND),
|
||||
("lpVerb", wintypes.LPCWSTR), ("lpFile", wintypes.LPCWSTR), ("lpParameters", wintypes.LPCWSTR),
|
||||
("lpDirectory", wintypes.LPCWSTR), ("nShow", ctypes.c_int), ("hInstApp", wintypes.HINSTANCE),
|
||||
("lpIDList", ctypes.c_void_p), ("lpClass", wintypes.LPCWSTR), ("hkeyClass", wintypes.HKEY),
|
||||
("dwHotKey", wintypes.DWORD), ("hIconOrMonitor", wintypes.HANDLE), ("hProcess", wintypes.HANDLE)]
|
||||
|
||||
SEE_MASK_NOCLOSEPROCESS, SW_HIDE = 0x40, 0
|
||||
info = SHELLEXECUTEINFOW(cbSize=ctypes.sizeof(SHELLEXECUTEINFOW), fMask=SEE_MASK_NOCLOSEPROCESS,
|
||||
lpVerb="runas", lpFile=program, lpParameters=params, lpDirectory=cwd, nShow=SW_HIDE)
|
||||
shell32 = ctypes.WinDLL("shell32", use_last_error=True)
|
||||
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
if not shell32.ShellExecuteExW(ctypes.byref(info)):
|
||||
return None # ERROR_CANCELLED (1223) or failure
|
||||
k32.WaitForSingleObject(info.hProcess, timeout_s * 1000)
|
||||
code = wintypes.DWORD()
|
||||
k32.GetExitCodeProcess(info.hProcess, ctypes.byref(code))
|
||||
k32.CloseHandle(info.hProcess)
|
||||
return code.value
|
||||
|
||||
|
||||
def _format_windows(disk: Disk, log: list[str]) -> Outcome:
|
||||
work = _workdir()
|
||||
script, result = work / "format_card.ps1", work / "result.json"
|
||||
script.write_text(windows_format_script(disk, result), encoding="utf-8-sig")
|
||||
result = _workdir() / "result.json"
|
||||
log.append("Asking Windows for administrator rights to format the card (UAC prompt)...")
|
||||
launcher = ("$p = Start-Process powershell -Verb RunAs -Wait -PassThru -WindowStyle Hidden "
|
||||
f"-ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \"{script}\"'; exit $p.ExitCode")
|
||||
proc = subprocess.run(["powershell", "-NoProfile", "-Command", launcher], capture_output=True, text=True,
|
||||
timeout=600, creationflags=subprocess.CREATE_NO_WINDOW)
|
||||
if not result.exists():
|
||||
err = (proc.stderr or "").strip().splitlines()
|
||||
cancelled = any("canceled" in e.lower() or "cancelled" in e.lower() for e in err)
|
||||
return Outcome(False, "Administrator permission was refused - the card was not formatted." if cancelled
|
||||
else "Formatting did not run: " + (err[-1] if err else "unknown error"), log=log)
|
||||
t = time.perf_counter()
|
||||
code = _run_elevated(*win_format_command(disk, result))
|
||||
if code is None:
|
||||
return Outcome(False, "Administrator permission was refused - the card was not formatted.", log=log)
|
||||
try:
|
||||
res = json.loads(result.read_text(encoding="utf-8-sig"))
|
||||
res = json.loads(result.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError) as exc:
|
||||
# The script ran but its report is unreadable: judge by the card itself.
|
||||
log.append(f"Could not read the format result ({exc.__class__.__name__}); checking the card directly.")
|
||||
drive, fs, label = _query_windows_card(int(disk.id))
|
||||
ok = bool(drive) and fs.upper() == "FAT32" and label == VOLUME_LABEL
|
||||
res = {"ok": ok, "drive": drive,
|
||||
"message": "Formatted FAT32 (MBR)." if ok else f"Card state after formatting: {fs or 'no file system'} "
|
||||
f"'{label}' - formatting probably failed."}
|
||||
log.append(res.get("message", ""))
|
||||
return Outcome(False, f"The formatter did not report back ({exc.__class__.__name__}, exit {code}). "
|
||||
"Check the card with 'Check card'.", log=log)
|
||||
steps = ", ".join(f"{k} {v:.1f} s" for k, v in res.get("timings", {}).items())
|
||||
log.append(f"{res.get('message', '')} ({time.perf_counter() - t:.1f} s incl. permission prompt; {steps})")
|
||||
return Outcome(bool(res.get("ok")), res.get("message", ""), res.get("drive", ""), log)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user