commit ce8b49d0ec960ef57d83f2054eee42cfd8cdcbe4 Author: Paravis Date: Sat Nov 7 20:40:47 2026 +0300 Paravis USB installer 2.0 (C++ port) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87920e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Build output +build/ +build_x64/ +build_x86/ +out/ +bin/ +obj/ +*.exe +*.dll +*.lib +*.exp +*.obj +*.pdb +*.ilk +*.res +*.tlog +*.idb + +# Installation images and archives (never committed) +Data.pf +*.wim +*.esd +*.iso +*.pf + +# CMake +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake + +# Visual Studio +.vs/ +*.vcxproj* +*.sln +*.user +*.suo + +# Logs, temporary and debug files +*.log +*.tmp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bba7ddb --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.20) + +project(ParavisInstallerCpp LANGUAGES CXX) + +enable_language(RC) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(ParavisInstaller WIN32 + src/main.cpp + src/install.cpp + src/disks.cpp + src/util.cpp + src/app.rc +) + +# Keep the installer self-contained: no VC++ redistributable required. +set_property(TARGET ParavisInstaller PROPERTY MSVC_RUNTIME_LIBRARY + "MultiThreaded$<$:Debug>") + +target_compile_definitions(ParavisInstaller PRIVATE + UNICODE + _UNICODE + WIN32_LEAN_AND_MEAN + NOMINMAX +) + +target_compile_options(ParavisInstaller PRIVATE + /utf-8 + /W4 + /permissive- +) + +target_link_libraries(ParavisInstaller PRIVATE + comctl32 + shell32 + user32 + gdi32 + advapi32 +) + +# The manifest is embedded through app.rc; do not let the linker add a second one. +target_link_options(ParavisInstaller PRIVATE /MANIFEST:NO) + +set_target_properties(ParavisInstaller PROPERTIES + OUTPUT_NAME "ParavisInstaller" +) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..814759f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Paravis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e066e7a --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Paravis installer — C++ port + +Native Win32 port of `USBInstaller\usb-install.au3` with the same layout, +behaviour and data flow. + +## Build + +Requirements: + +- Visual Studio 2022 with the Desktop C++ workload +- CMake 3.20 or newer + +From this directory run: + +```bat +build.cmd +``` + +The executable is produced at: + +``` +build\Release\ParavisInstaller.exe +``` + +The target is 32-bit (`Win32`) like the original AutoIt build, and the +application manifest requests administrator rights. + +## What is implemented + +- 376 × 183 custom-drawn window, header, custom disk dropdown, status line, + progress bar, install/cancel button and hover/pressed states. +- USB disk enumeration by storage bus type, nominal size formatting and + Windows-system-disk protection. +- Diskpart layout: FAT32 PARAVIS 16 GB, FAT VTOYEFI 32 MB, exFAT DATA + (remaining space minus 3 MB). +- Offline disk is brought online before partitioning. +- Layout verification from the partition table and volume labels. +- DISM apply of images 1 → PARAVIS, 2 → VTOYEFI, 3 → DATA with progress + parsing and cancellation of the DISM process tree. +- Pressing the cancel button during installation asks for confirmation first. +- VTOYEFI drive letter removal after a successful install. +- Success warning, launch of `Change_BootLoader.exe` from the PARAVIS + partition, then immediate exit. +- If a `Change_BootLoader.exe` is present next to the installer, it is copied + to the PARAVIS partition after unpacking and used instead of the copy from + `Data.pf`. +- Service modes: `/selftest`, `/verify=N`, `/testdisk=N`, `/wim=path`. + +## Notes + +- `Data.pf` is searched next to the executable, exactly like the AutoIt + version. `/wim=` remains a service/testing override. +- DISM command lines are built without the outer `cmd /c "..."` quote pair, + so paths containing spaces work correctly. +- The debug log is disabled by default. Set the environment variable + `PARAVIS_DEBUG_LOG=1` to write `%TEMP%\paravis_cpp_debug.log`. +- Message boxes use the native `MessageBoxW` dialog; text, buttons and + behavior are the same as the AutoIt version. + +## Repository layout + +``` +src/ C++ sources and resources +legacy-au3/ original AutoIt script +build.cmd release build +``` + +## License + +MIT. See [`LICENSE`](LICENSE). + +This repository contains source code only. Installation archives +(`Data.pf`), Windows images (`*.wim`, `*.esd`) and third-party binaries are +not distributed here. diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..39c247a --- /dev/null +++ b/build.cmd @@ -0,0 +1,9 @@ +@echo off +setlocal +cd /d "%~dp0" +cmake -S . -B build -G "Visual Studio 17 2022" -A Win32 +if errorlevel 1 exit /b 1 +cmake --build build --config Release +if errorlevel 1 exit /b 1 +echo. +echo Built: %~dp0build\Release\ParavisInstaller.exe diff --git a/legacy-au3/README-legacy.md b/legacy-au3/README-legacy.md new file mode 100644 index 0000000..7e1dd84 --- /dev/null +++ b/legacy-au3/README-legacy.md @@ -0,0 +1,64 @@ +# PARAVIS FLASH — утилита разметки USB-диска (legacy AutoIt) + +Исходный текст оригинальной версии на AutoIt (`usb-install.au3`). +Поддерживаемая версия — C++-порт в корне репозитория (`src/`, `CMakeLists.txt`). + +`Paravis installer.exe` — 32-битная сборка (Windows 8 и новее, x86/x64). + +## Что делает + +Размечает выбранный USB-диск под мультизагрузочную флешку: + +| # | Размер | ФС | Метка | Назначение | +|---|--------|-----|-------|------------| +| 1 | 16 384 МБ (16 ГБ) | FAT32 | `PARAVIS` | основной раздел, активный (bootmgr / g4d) | +| 2 | 32 МБ | FAT | `VTOYEFI` | раздел Ventoy | +| 3 | остальное − 1 − 16384 − 32 − 3 МБ | exFAT | `DATA` | данные | +| — | 3 МБ в конце | — | — | нераспределённые (нужны Ventoy; в интерфейсе не показываются) | + +Разметка — MBR (`convert mbr`). Все данные на диске уничтожаются +(есть подтверждение, по умолчанию «Нет»). + +## Распаковка файлов из архива + +После успешной разметки и проверки раскладки утилита распаковывает архив +**`Data.pf`** (лежит рядом с exe) на созданные разделы: + +| образ | раздел | содержимое | +|---|---|---| +| `1-PARAVIS` | 1-й (FAT32, PARAVIS) | программа, g4d, Xorboot, KRD, Strelec, MSDaRT, UBCD | +| `2-VTOYEFI` | 2-й (FAT, VTOYEFI) | Ventoy | +| `3-DATA` | 3-й (exFAT, DATA) | пусто (структура `ISO\Ventoy`) | + +Где ищется архив: + +* **только рядом с exe**, файл с именем **`Data.pf`**: `<папка exe>\Data.pf`; +* никакого поиска по другим папкам и никаких диалогов выбора — если файла + нет, утилита сообщает, что архив не найден, и файлы не копирует + (разметка при этом уже выполнена); +* служебный параметр `/wim=<путь>` оставлен только для автотестов. + +Раздел определяется по номеру раздела на выбранном диске +(`Win32_DiskPartition.DiskIndex/Index` + ассоциация +`Win32_LogicalDiskToPartition`), а не по букве. + +Архив `Data.pf` — это обычный WIM без сжатия (`/Compress:none`), только под +другим именем. DISM применяет его так же, как `.wim`. + +## Служебные ключи (не для обычного запуска) + +| Ключ | Назначение | +|------|------------| +| `/selftest` | пишет `%TEMP%\paravis_selftest.txt` (Windows-dir, номера системных дисков, admin) и выходит | +| `/verify=N` | пишет результат проверки раскладки диска N в `%TEMP%\paravis_verify.txt` (только чтение) | +| `/testdisk=N` | работает с диском N без выбора в списке — только для автотестов на VHD | +| `/wim=<путь>` | путь к архиву (по умолчанию — `Data.pf` рядом с exe) | + +## Сборка + +``` +"C:\Program Files (x86)\AutoIt3\Aut2Exe\Aut2exe.exe" /in usb-install.au3 /out usb-install.exe /icon ..\..\windows-installer\sec.ico /x86 +``` + +Автотесты (`driver*.au3`, `watch.au3`, скрипты PowerShell) в этот +репозиторий не входят: это были локальные стенды для VHD и реальной флешки. diff --git a/legacy-au3/theme.manifest b/legacy-au3/theme.manifest new file mode 100644 index 0000000..2d53249 --- /dev/null +++ b/legacy-au3/theme.manifest @@ -0,0 +1,15 @@ + + + + PARAVIS FLASH installer + + + + + + + + true + + + diff --git a/legacy-au3/usb-install.au3 b/legacy-au3/usb-install.au3 new file mode 100644 index 0000000..837eae8 --- /dev/null +++ b/legacy-au3/usb-install.au3 @@ -0,0 +1,1191 @@ +; PARAVIS FLASH - разметка USB-диска (однокнопочный вариант) +; 1) FAT32 16 ГБ (PARAVIS) +; 2) FAT 32 МБ (VTOYEFI, под Ventoy) +; 3) exFAT остальное минус 3 МБ (DATA) +; В конце диска остаются 3 МБ нераспределёнными. +; ВНИМАНИЕ: все данные на выбранном диске уничтожаются. +#RequireAdmin +#NoTrayIcon +#include +#include +#include +#include +#include +#include + +Opt("GUIOnEventMode", 1) +Opt("MustDeclareVars", 1) + +Global Const $MB = 1048576 +Global Const $P1_MB = 16384 +Global Const $P2_MB = 32 +Global Const $TAIL_MB = 3 + +Global $gTestDisk = -1 ; служебный параметр /testdisk=N (только для автотеста) +Global $gWimPath = "" ; путь к архиву (параметр /wim=..., иначе рядом с exe) +Global $gRun = StringFormat("%04d%02d%02d_%02d%02d%02d", @YEAR, @MON, @MDAY, @HOUR, @MIN, @SEC) ; метка запуска для логов +Global $gFailInfo = "" ; на каком шаге и на какой раздел шла запись +Global $gLastOut = "" ; последний вывод внешней команды (для разбора ошибок) + +; Служебные режимы (не для обычного запуска): +; /selftest - записать диагностику в %TEMP%\paravis_selftest.txt и выйти +; /testdisk=N - работать с диском N, не требуя выбора в списке (автотест на VHD) +If $CmdLine[0] > 0 Then + Local $ai, $arg, $argl + For $ai = 1 To $CmdLine[0] + $arg = $CmdLine[$ai] + $argl = StringLower($arg) + + If $argl = "/selftest" Then + Local $hSf = FileOpen(@TempDir & "\paravis_selftest.txt", $FO_OVERWRITE + $FO_ANSI) + FileWriteLine($hSf, "windows=" & @WindowsDir) + FileWriteLine($hSf, "system_disks=" & _SystemDiskIds()) + FileWriteLine($hSf, "admin=" & IsAdmin()) + FileWriteLine($hSf, "wim=" & @ScriptDir & "\Data.pf") + FileWriteLine($hSf, "wim_exists=" & FileExists(@ScriptDir & "\Data.pf")) + FileClose($hSf) + Exit + EndIf + + If StringLeft($argl, 8) = "/verify=" Then + ; Диагностика проверки раскладки на уже готовом диске (только чтение). + Local $vd = Number(StringTrimLeft($arg, 8)) + Local $vok = False + Local $vtxt = _VerifyLayout($vd, $vok) + Local $vf = FileOpen(@TempDir & "\paravis_verify.txt", $FO_OVERWRITE + $FO_ANSI) + FileWriteLine($vf, "disk=" & $vd & " ok=" & $vok) + FileWriteLine($vf, $vtxt) + FileClose($vf) + Exit + EndIf + + If StringLeft($argl, 10) = "/testdisk=" Then $gTestDisk = Number(StringTrimLeft($arg, 10)) + If StringLeft($argl, 5) = "/wim=" Then $gWimPath = StringTrimLeft($arg, 5) + Next +EndIf + +Global $hStatus, $hGo, $hProg, $hWork, $hHint +Global $gBusy = False ; идёт разметка/запись +Global $gCanInstall = False ; можно ли устанавливать на выбранный диск +Global $gModal = False ; открыто модальное окно +Global $aDiskNum[0] ; номера дисков в порядке выпадающего списка +Global $aDiskSize[0] ; их размеры на момент заполнения списка +Global $aDiskName[0] ; названия моделей +Global $aDiskCap[0] ; объёмы для показа +Global $gSelIdx = 0 ; выбранный индекс +Global $gSig = "-" ; подпись текущего состава устройств (на старте всегда отличается) +Global $hPopup = 0 ; окно раскрытого списка +Global $hItemName[0] ; пункты: название +Global $hItemCap[0] ; пункты: объём +Global $aItemBg[0] ; последние цвета пунктов +Global $gDropGuard = 0 ; защита от закрытия сразу после открытия +Global $gNeedScan = False ; пришло сообщение о смене устройств +Global $gDevTick = 0 ; когда пришло это сообщение +Global $gCancel = False ; пользователь нажал «ОТМЕНИТЬ» +Global $gCancelDown = False +Global $gCancelBg = -1 +Global $gApplied = 0 ; сколько образов уже распаковано +Global $gPhase = 0 ; 0 — простой, 1 — разметка, 2 — копирование файлов +Global $gRunPid = 0 ; процесс текущей команды (для отмены копирования) +Global $gInstalled = False ; на выбранном диске уже стоит сборка (маркеры на месте) +Global $gClickGuard = 0 ; защита от «залипшего» клика после закрытия окна + +; --- оформление +Global Const $CLR_BG = 0xF7F8FA +Global Const $CLR_HEAD = 0x1E2A44 +Global Const $CLR_DIM = 0x6B7280 + + +Global $hGUI = GUICreate("Paravis installer", 376, 183, -1, -1, $WS_POPUP, $WS_EX_APPWINDOW) +GUISetBkColor($CLR_BG, $hGUI) +GUISetOnEvent($GUI_EVENT_CLOSE, "_Quit") +GUISetFont(9, 400, 0, "Tahoma", $hGUI) + +; шапка +Global $hHead = GUICtrlCreateLabel("", 0, 0, 376, 42) +GUICtrlSetBkColor($hHead, $CLR_HEAD) +Global $hTitle = GUICtrlCreateLabel("Paravis Flash", 16, 2, 300, 24) +GUICtrlSetFont($hTitle, 15, 700, 0, "Tahoma") +GUICtrlSetColor($hTitle, 0xFFFFFF) +GUICtrlSetBkColor($hTitle, $CLR_HEAD) +Global $hSub = GUICtrlCreateLabel("Запись загрузочной флешки", 18, 26, 300, 14) +GUICtrlSetColor($hSub, 0xC7D0E0) +GUICtrlSetBkColor($hSub, $CLR_HEAD) + +Global $hMin = GUICtrlCreateLabel("—", 306, 2, 30, 18, BitOR($SS_CENTER, $SS_CENTERIMAGE, $SS_NOTIFY)) +GUICtrlSetFont($hMin, 10, 400, 0, "Tahoma") +GUICtrlSetColor($hMin, 0xC7D0E0) +GUICtrlSetBkColor($hMin, $CLR_HEAD) + +Global $hCloseBtn = GUICtrlCreateLabel("✕", 338, 2, 30, 18, BitOR($SS_CENTER, $SS_CENTERIMAGE, $SS_NOTIFY)) +GUICtrlSetFont($hCloseBtn, 9, 400, 0, "Tahoma") +GUICtrlSetColor($hCloseBtn, 0xC7D0E0) +GUICtrlSetBkColor($hCloseBtn, $CLR_HEAD) + +; карточка выбора +Local $hCard = GUICtrlCreateLabel("", 8, 45, 360, 46) +GUICtrlSetBkColor($hCard, 0xFFFFFF) +Global $hLbl = GUICtrlCreateLabel("Выбран накопитель:", 16, 45, 340, 15) +GUICtrlSetColor($hLbl, $CLR_DIM) +GUICtrlSetBkColor($hLbl, 0xFFFFFF) + +Global $hComboFrame = GUICtrlCreateLabel("", 15, 61, 342, 28) +GUICtrlSetBkColor($hComboFrame, 0xD5DAE3) +Global $hDrop = GUICtrlCreateLabel("", 16, 62, 340, 26) +GUICtrlSetBkColor($hDrop, 0xFFFFFF) +Global $hDropName = GUICtrlCreateLabel("", 21, 62, 240, 26, BitOR($SS_LEFT, $SS_CENTERIMAGE, $SS_NOTIFY)) +GUICtrlSetFont($hDropName, 9, 400, 0, "Tahoma") +GUICtrlSetColor($hDropName, 0x1F2430) +GUICtrlSetBkColor($hDropName, 0xFFFFFF) +Global $hDropSize = GUICtrlCreateLabel("", 262, 62, 74, 26, BitOR($SS_RIGHT, $SS_CENTERIMAGE, $SS_NOTIFY)) +GUICtrlSetFont($hDropSize, 9, 400, 0, "Tahoma") +GUICtrlSetColor($hDropSize, 0x1F2430) +GUICtrlSetBkColor($hDropSize, 0xFFFFFF) +GUICtrlSetFont($hDrop, 9, 400, 0, "Tahoma") +GUICtrlSetColor($hDrop, 0x1F2430) +GUICtrlSetBkColor($hDrop, 0xFFFFFF) +Global $hArrow = GUICtrlCreateLabel("▼", 336, 62, 16, 26, BitOR($SS_CENTER, $SS_CENTERIMAGE, $SS_NOTIFY)) +GUICtrlSetFont($hArrow, 8, 400, 0, "Tahoma") +GUICtrlSetColor($hArrow, 0x6B7280) +GUICtrlSetBkColor($hArrow, 0xFFFFFF) +GUICtrlSetOnEvent($hDrop, "_DropToggle") +GUICtrlSetOnEvent($hArrow, "_DropToggle") + + +$hStatus = GUICtrlCreateLabel("", 14, 95, 348, 15) +GUICtrlSetColor($hStatus, $CLR_DIM) +GUICtrlSetBkColor($hStatus, $CLR_BG) + +$hProg = GUICtrlCreateProgress(14, 139, 348, 5) +GUICtrlSetState($hProg, $GUI_HIDE) + +; текст установки — на месте красного предупреждения +$hWork = GUICtrlCreateLabel("", 14, 115, 348, 18, $SS_CENTER) +GUICtrlSetFont($hWork, 9, 700, 0, "Tahoma") +GUICtrlSetColor($hWork, 0x1F2430) +GUICtrlSetBkColor($hWork, $CLR_BG) +GUICtrlSetState($hWork, $GUI_HIDE) + +$hGo = GUICtrlCreateLabel("УСТАНОВИТЬ", 14, 147, 348, 30, BitOR($SS_CENTER, $SS_CENTERIMAGE, $SS_NOTIFY)) +GUICtrlSetFont($hGo, 10, 700, 0, "Tahoma") +GUICtrlSetColor($hGo, 0xFFFFFF) +GUICtrlSetBkColor($hGo, 0x2563EB) +; нажатие на кнопку обрабатывается опросом (_GoPoll), а не событием AutoIt + +$hHint = GUICtrlCreateLabel("Все данные на выбранном диске будут уничтожены", 14, 115, 348, 18, $SS_CENTER) +GUICtrlSetColor($hHint, 0xC0392B) +GUICtrlSetFont($hHint, 9, 700, 0, "Tahoma") +GUICtrlSetBkColor($hHint, $CLR_BG) + +GUISetState(@SW_SHOW, $hGUI) +AdlibRegister("_BtnPoll", 40) +AdlibRegister("_AutoRefresh", 250) +GUIRegisterMsg(0x0219, "_OnDeviceChange") +_Refresh() + +While 1 + Sleep(50) +WEnd + +Func _Quit() + Exit +EndFunc + +Func _Refresh() + _ScanDisks() +EndFunc + +Func _AutoRefresh() + ; Список перечитывается только когда система сообщила о смене устройств. + ; Постоянного опроса нет — окно не подтормаживает при перетаскивании и наведении. + If Not $gNeedScan Then Return + If $gBusy Or $gModal Or $hPopup <> 0 Then Return + If TimerDiff($gDevTick) < 500 Then Return + $gNeedScan = False + _ScanDisks() +EndFunc + +Func _OnDeviceChange($hWnd, $iMsg, $wParam, $lParam) + ; WM_DEVICECHANGE: подключили/отключили накопитель + If $wParam = 0x8000 Or $wParam = 0x8004 Or $wParam = 0x0007 Then + If Not $gNeedScan Then + $gNeedScan = True + $gDevTick = TimerInit() + EndIf + EndIf + Return $GUI_RUNDEFMSG +EndFunc + +Func _ScanDisks() + ; Перечитывает USB-диски. Если состав не изменился — ничего не трогает. + Local $sig = "", $n = 0 + Local $nums[0], $sizes[0], $names[0], $caps[0] + + Local $oStorage = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\Microsoft\Windows\Storage") + If IsObj($oStorage) Then + Local $col = $oStorage.ExecQuery("SELECT * FROM MSFT_Disk") + If IsObj($col) Then + For $d In $col + If Number($d.BusType) = 7 Then + _PushDisk($nums, $sizes, $names, $caps, $n, Number($d.Number), Number($d.Size), $d.FriendlyName) + $sig = $sig & Number($d.Number) & ":" & Number($d.Size) & "|" + EndIf + Next + EndIf + EndIf + + If $n = 0 Then + ; страховка: если провайдер хранилища недоступен + Local $oWMI = ObjGet("winmgmts:\\.\root\cimv2") + If IsObj($oWMI) Then + Local $col2 = $oWMI.ExecQuery("SELECT * FROM Win32_DiskDrive WHERE InterfaceType='USB' OR PNPDeviceID LIKE 'USBSTOR%' OR MediaType='External hard disk media' OR MediaType='Removable Media'") + If IsObj($col2) Then + For $d In $col2 + Local $num = Number(StringRegExpReplace($d.DeviceID, ".*PHYSICALDRIVE(\d+).*", "$1")) + _PushDisk($nums, $sizes, $names, $caps, $n, $num, Number($d.Size), $d.Model) + $sig = $sig & $num & ":" & Number($d.Size) & "|" + Next + EndIf + EndIf + EndIf + + If $sig = $gSig Then Return ; состав тот же — экран не меняем + $gSig = $sig + + ; запоминаем выбранный диск, чтобы не сбить выбор + Local $curNum = -1 + If UBound($aDiskNum) > 0 And $gSelIdx >= 0 And $gSelIdx < UBound($aDiskNum) Then $curNum = $aDiskNum[$gSelIdx] + + $aDiskNum = $nums + $aDiskSize = $sizes + $aDiskName = $names + $aDiskCap = $caps + + If $n = 0 Then + GUICtrlSetData($hDropName, "") + GUICtrlSetData($hDropSize, "") + GUICtrlSetData($hLbl, "Подходящие накопители отсутствуют!") + GUICtrlSetData($hStatus, "Найдено: 0") + $gSelIdx = 0 + $gInstalled = False + _ArrowState() + _RefreshGo() + Return + EndIf + + Local $found = 0, $i + For $i = 0 To UBound($aDiskNum) - 1 + If $aDiskNum[$i] = $curNum Then + $found = $i + ExitLoop + EndIf + Next + $gSelIdx = $found + _ShowSelected() + _RefreshGo() + GUICtrlSetData($hLbl, "Выбран накопитель:") + GUICtrlSetData($hStatus, "Найдено: " & UBound($aDiskNum)) + _ArrowState() +EndFunc + +Func _PushDisk(ByRef $nums, ByRef $sizes, ByRef $names, ByRef $caps, ByRef $n, $num, $size, $model) + ReDim $nums[$n + 1] + ReDim $sizes[$n + 1] + ReDim $names[$n + 1] + ReDim $caps[$n + 1] + $nums[$n] = $num + $sizes[$n] = $size + Local $m = StringStripWS(String($model), 3) + If $m = "" Then $m = "(без названия)" + $names[$n] = $m + $caps[$n] = _NominalSize($size) + $n = $n + 1 +EndFunc + +Func _ShowSelected() + If UBound($aDiskName) = 0 Then Return + GUICtrlSetData($hDropName, $aDiskName[$gSelIdx]) + GUICtrlSetData($hDropSize, $aDiskCap[$gSelIdx]) + _CheckInstalled() +EndFunc + + +Func _NominalGb($bytes) + ; «Магазинный» объём в ГБ (64, а не 60) + Local $gb = Number($bytes) / 1000000000 + Local $list[23] = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1000, 1500, 2000, 3000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000] + Local $i + For $i = 0 To UBound($list) - 1 + If $gb <= $list[$i] * 1.02 Then Return $list[$i] + Next + Return $gb +EndFunc + +Func _NominalSize($bytes) + Local $n = _NominalGb($bytes) + If $n >= 1000 Then Return StringFormat("%g ТБ", $n / 1000) + Return StringFormat("%d ГБ", $n) +EndFunc + +Func _SelectedIndex() + If UBound($aDiskNum) = 0 Then Return -1 + If $gSelIdx < 0 Or $gSelIdx >= UBound($aDiskNum) Then Return -1 + Return $gSelIdx +EndFunc + +; --- свой выпадающий список +Func _DropToggle() + If $gBusy Or $gModal Then Return + If UBound($aDiskName) = 0 Then Return + If $hPopup <> 0 Then + _DropClose() + Else + _DropOpen() + EndIf +EndFunc + +Func _DropOpen() + Local $n = UBound($aDiskName) + If $n = 0 Then Return + Local $itemH = 24 + Local $p = WinGetPos($hGUI) + Local $c = WinGetClientSize($hGUI) + If Not IsArray($p) Or Not IsArray($c) Then Return + Local $borderX = ($p[2] - $c[0]) / 2 + Local $titleH = $p[3] - $c[1] - $borderX + + ; рамка — как у остальных элементов: 1 px светло-серым, без системной рамки + $hPopup = GUICreate("", 342, $n * $itemH + 2, $p[0] + $borderX + 15, $p[1] + $titleH + 89, $WS_POPUP, $WS_EX_TOOLWINDOW, $hGUI) + GUISetBkColor(0xD5DAE3, $hPopup) + Local $hPanel = GUICtrlCreateLabel("", 1, 1, 340, $n * $itemH) + GUICtrlSetBkColor($hPanel, 0xFFFFFF) + + ReDim $hItemName[$n] + ReDim $hItemCap[$n] + ReDim $aItemBg[$n] + Local $i + For $i = 0 To $n - 1 + Local $y = 1 + $i * $itemH + $hItemName[$i] = GUICtrlCreateLabel(" " & $aDiskName[$i], 1, $y, 248, $itemH, BitOR($SS_LEFT, $SS_CENTERIMAGE, $SS_NOTIFY)) + GUICtrlSetFont($hItemName[$i], 9, 400, 0, "Tahoma") + GUICtrlSetColor($hItemName[$i], 0x1F2430) + GUICtrlSetBkColor($hItemName[$i], 0xFFFFFF) + GUICtrlSetOnEvent($hItemName[$i], "_DropPick") + $hItemCap[$i] = GUICtrlCreateLabel($aDiskCap[$i] & " ", 249, $y, 92, $itemH, BitOR($SS_RIGHT, $SS_CENTERIMAGE, $SS_NOTIFY)) + GUICtrlSetFont($hItemCap[$i], 9, 400, 0, "Tahoma") + GUICtrlSetColor($hItemCap[$i], 0x6B7280) + GUICtrlSetBkColor($hItemCap[$i], 0xFFFFFF) + GUICtrlSetOnEvent($hItemCap[$i], "_DropPick") + $aItemBg[$i] = -1 + Next + $gDropGuard = TimerInit() + GUISetState(@SW_SHOW, $hPopup) +EndFunc + +Func _DropClose() + If $hPopup <> 0 Then + GUIDelete($hPopup) + $hPopup = 0 + EndIf +EndFunc + +Func _DropPick() + Local $i + For $i = 0 To UBound($aDiskName) - 1 + If @GUI_CtrlId = $hItemName[$i] Or @GUI_CtrlId = $hItemCap[$i] Then + $gSelIdx = $i + _ShowSelected() + _RefreshGo() + ExitLoop + EndIf + Next + _DropClose() +EndFunc + + +Func _DiskSize($num) + Local $sz = 0 + Local $oStorage = ObjGet("winmgmts:{impersonationLevel=impersonate}!\\.\root\Microsoft\Windows\Storage") + If IsObj($oStorage) Then + Local $col = $oStorage.ExecQuery("SELECT * FROM MSFT_Disk") + If IsObj($col) Then + For $d In $col + If Number($d.Number) = $num Then $sz = Number($d.Size) + Next + EndIf + EndIf + If $sz = 0 Then + Local $oWMI = ObjGet("winmgmts:\\.\root\cimv2") + If IsObj($oWMI) Then + Local $col2 = $oWMI.ExecQuery("SELECT * FROM Win32_DiskDrive WHERE Index=" & $num) + If IsObj($col2) Then + For $d In $col2 + $sz = Number($d.Size) + Next + EndIf + EndIf + EndIf + Return $sz +EndFunc + +Func _SystemDiskIds() + ; Возвращает строку вида "|0|" со номерами дисков, на которых лежит Windows. + Local $ids = "" + Local $oWMI = ObjGet("winmgmts:\\.\root\cimv2") + If Not IsObj($oWMI) Then Return "" + Local $sys = StringLeft(@WindowsDir, 2) + If StringRight($sys, 1) <> ":" Then Return "" + Local $col = $oWMI.ExecQuery("ASSOCIATORS OF {Win32_LogicalDisk.DeviceID='" & $sys & "'} WHERE AssocClass=Win32_LogicalDiskToPartition") + If Not IsObj($col) Then Return "" + For $part In $col + Local $m = StringRegExp(String($part.DeviceID), "(?i)Disk #(\d+)", 1) + If IsArray($m) Then $ids = $ids & "|" & Number($m[0]) & "|" + Next + Return $ids +EndFunc + +Func _Apply() + If $gBusy Then Return + If Not $gCanInstall And $gTestDisk < 0 Then Return + Local $disk = $gTestDisk + Local $expectSize = 0 + If $disk < 0 Then + Local $idx = _SelectedIndex() + If $idx < 0 Then + _Msg(BitOR($MB_ICONWARNING, $MB_OK), "Устройство не выбрано", "Выберите флешку в списке.") + Return + EndIf + $disk = $aDiskNum[$idx] + $expectSize = $aDiskSize[$idx] + EndIf + + ; Страховка: никогда не размечаем диск, на котором стоит Windows. + Local $sysIds = _SystemDiskIds() + If StringInStr($sysIds, "|" & $disk & "|") > 0 Then + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Это системный диск", _ + "Диск " & $disk & " содержит Windows. Разметка отменена - выберите другой диск.") + Return + EndIf + + + Local $sizeBytes = _DiskSize($disk) + If $sizeBytes <= 0 Then + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Ошибка", "Не удалось определить размер диска " & $disk & ".") + Return + EndIf + + ; Сверка: диск должен быть тем же, что был в списке (защита от сдвига индексов) + If $expectSize > 0 And Abs($sizeBytes - $expectSize) > ($expectSize / 100) Then + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Диск не совпадает", _ + "Размер выбранного диска (" & Round($sizeBytes / 1073741824, 1) & " ГБ) не совпадает с тем, " & _ + "что был в списке (" & Round($expectSize / 1073741824, 1) & " ГБ)." & @CRLF & @CRLF & _ + "Нажмите «Обновить» и выберите устройство заново.") + Return + EndIf + + Local $totalMB = Int($sizeBytes / $MB) + Local $p3 = $totalMB - 1 - $P1_MB - $P2_MB - $TAIL_MB + + If $p3 < 512 Then + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Слишком маленький диск", _ + "Нужен диск не меньше " & ($P1_MB + $P2_MB + $TAIL_MB + 512) & " МБ." & @CRLF & _ + "Сейчас: " & $totalMB & " МБ.") + Return + EndIf + + Local $msg = "Диск " & $disk & " (" & StringFormat("%.1f", $sizeBytes / 1073741824) & " ГБ)" & @CRLF & @CRLF & _ + "Будет создано:" & @CRLF & _ + " 1) FAT32 16 ГБ метка PARAVIS" & @CRLF & _ + " 2) FAT 32 МБ метка VTOYEFI" & @CRLF & _ + " 3) exFAT " & StringFormat("%.1f", $p3 / 1024) & " ГБ метка DATA" & @CRLF & @CRLF & _ + "Затем на разделы будут распакованы файлы из архива Data.pf" & @CRLF & _ + "(1 -> PARAVIS, 2 -> VTOYEFI, 3 -> DATA)." & @CRLF & @CRLF & _ + "ВНИМАНИЕ! ВСЕ ДАННЫЕ НА ЭТОМ ДИСКЕ БУДУТ УНИЧТОЖЕНЫ." & @CRLF & _ + "Продолжить?" + + GUICtrlSetData($hGo, "Ожидание подтверждения...") + GUICtrlSetBkColor($hGo, 0xAEB6C4) + GUICtrlSetColor($hGo, 0xEDEFF3) + If _Msg(BitOR($MB_ICONWARNING, $MB_YESNO, $MB_DEFBUTTON2), "Подтверждение", $msg) <> $IDYES Then + _GoCaption() + _RefreshGo() + Return + EndIf + + _BusySet(True) + $gPhase = 1 + + ; Если диск был offline (например, после тестов ВМ) — включаем его перед разметкой. + ; На уже online-диске diskpart вернёт ошибку, но это не помешает основному скрипту. + Local $onlineTmp = @TempDir & "\paravis_" & $gRun & "_online.txt" + Local $hOn = FileOpen($onlineTmp, $FO_OVERWRITE + $FO_ANSI) + If $hOn <> -1 Then + FileWrite($hOn, "select disk " & $disk & @CRLF & "online disk" & @CRLF & "exit" & @CRLF) + FileClose($hOn) + RunWait(@ComSpec & ' /c diskpart.exe /s "' & $onlineTmp & '" > nul 2>&1', "", @SW_HIDE) + FileDelete($onlineTmp) + EndIf + + Local $script = "select disk " & $disk & @CRLF & _ + "clean" & @CRLF & _ + "convert mbr" & @CRLF & _ + "create partition primary size=" & $P1_MB & @CRLF & _ + "format fs=fat32 quick label=""PARAVIS""" & @CRLF & _ + "active" & @CRLF & "assign" & @CRLF & _ + "create partition primary size=" & $P2_MB & @CRLF & _ + "format fs=fat quick label=""VTOYEFI""" & @CRLF & _ + "assign" & @CRLF & _ + "create partition primary size=" & $p3 & @CRLF & _ + "format fs=exfat quick label=""DATA""" & @CRLF & _ + "assign" & @CRLF & "exit" & @CRLF + + Local $tmp = @TempDir & "\paravis_" & $gRun & "_diskpart.txt" + Local $hFile = FileOpen($tmp, $FO_OVERWRITE + $FO_ANSI) + If $hFile = -1 Then + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Ошибка", "Не удалось создать файл скрипта.") + Return + EndIf + FileWrite($hFile, $script) + FileClose($hFile) + + GUICtrlSetData($hWork, "Разметка диска " & $disk & "…") + + Local $logFile = @TempDir & "\paravis_" & $gRun & "_diskpart_log.txt" + ; Разметку не прерываем: ждём её конца, но нажатие «ОТМЕНИТЬ» запоминаем + Local $pidDp = Run(@ComSpec & ' /c diskpart.exe /s "' & $tmp & '" > "' & $logFile & '" 2>&1', "", @SW_HIDE) + If $pidDp Then + While ProcessExists($pidDp) + _CancelPoll() + Sleep(50) + WEnd + EndIf + + Local $log = "" + If FileExists($logFile) Then $log = FileRead($logFile) + + Sleep(1500) + Local $ok = False + Local $check = _VerifyLayout($disk, $ok) + + If Not $ok Then + GUICtrlSetData($hWork, "Раскладка не совпала с ожидаемой") + GUICtrlSetState($hProg, $GUI_HIDE) + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Ошибка разметки", _ + "РАСКЛАДКА НЕ СОВПАЛА С ОЖИДАЕМОЙ" & @CRLF & @CRLF & $check & @CRLF & @CRLF & _ + "Лог: " & $logFile & @CRLF & @CRLF & StringLeft($log, 600)) + _BusySet(False) + GUICtrlSetState($hWork, $GUI_HIDE) + GUICtrlSetState($hHint, $GUI_SHOW) + _Refresh() + Return + EndIf + + ; --- разметка верна, распаковываем образы на разделы + If $gCancel Then + _BusySet(False) + GUICtrlSetState($hWork, $GUI_HIDE) + GUICtrlSetState($hHint, $GUI_SHOW) + _Refresh() + _Msg(BitOR($MB_ICONINFORMATION, $MB_OK), "Отменено", _ + "Операция отменена пользователем." & @CRLF & @CRLF & _ + "Диск размечен, файлы не скопированы.") + Return + EndIf + + Local $wim = _FindWim() + If $wim = "" Then + GUICtrlSetData($hWork, "Размечено, архив Data.pf не найден") + _Msg(BitOR($MB_ICONWARNING, $MB_OK), "Разметка выполнена", _ + "Диск размечен, раскладка верна." & @CRLF & @CRLF & $check & @CRLF & @CRLF & _ + "Но файл Data.pf не найден рядом с программой - файлы не скопированы.") + _BusySet(False) + GUICtrlSetState($hWork, $GUI_HIDE) + GUICtrlSetState($hHint, $GUI_SHOW) + _Refresh() + Return + EndIf + + $gPhase = 2 + Local $res = _ApplyImages($disk, $wim) + $gPhase = 0 + + If $res = 1 Then + _HideVentoy($disk) + GUICtrlSetData($hWork, "Готово") + GUICtrlSetState($hProg, $GUI_HIDE) + + ; После успешной установки — предупреждение и запуск установщика загрузчиков + _Msg(BitOR($MB_ICONINFORMATION, $MB_OK), "Установка завершена", _ + "Установка завершена успешно. Сейчас будет запущен установщик загрузчиков. " & _ + "Выберите Legacy и UEFI загрузчики. Если этого не сделать, то будет работать только UEFI.") + + Local $p1 = _PartitionLetter($disk, 0) + If $p1 <> "" Then + Local $loader = $p1 & "Change_BootLoader.exe" + If FileExists($loader) Then + Local $pidLoader = Run('"' & $loader & '"') + If $pidLoader <> 0 Then Exit + _Msg(BitOR($MB_ICONWARNING, $MB_OK), "Установщик загрузчиков", _ + "Не удалось запустить Change_BootLoader.exe." & @CRLF & @CRLF & _ + "Путь: " & $loader) + Else + _Msg(BitOR($MB_ICONWARNING, $MB_OK), "Установщик загрузчиков", _ + "Файл Change_BootLoader.exe не найден на разделе PARAVIS." & @CRLF & @CRLF & _ + "Путь: " & $loader) + EndIf + Else + _Msg(BitOR($MB_ICONWARNING, $MB_OK), "Установщик загрузчиков", _ + "Не удалось определить букву раздела PARAVIS.") + EndIf + ElseIf $res = 2 Then + GUICtrlSetData($hWork, "Ошибка записи") + GUICtrlSetState($hProg, $GUI_HIDE) + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Ошибка копирования файлов", _ + "Диск размечен, но скопировать файлы не удалось." & @CRLF & @CRLF & _ + "Запустите установку ещё раз.") + ElseIf $res = 3 Then + GUICtrlSetData($hWork, "Отменено") + GUICtrlSetState($hProg, $GUI_HIDE) + _BusySet(False) + GUICtrlSetState($hWork, $GUI_HIDE) + GUICtrlSetState($hHint, $GUI_SHOW) + _Refresh() + _Msg(BitOR($MB_ICONINFORMATION, $MB_OK), "Отменено", _ + "Операция отменена пользователем." & @CRLF & @CRLF & _ + "Диск размечен, скопировано образов: " & $gApplied & " из 3.") + Return + EndIf + + _BusySet(False) + GUICtrlSetState($hWork, $GUI_HIDE) + GUICtrlSetState($hHint, $GUI_SHOW) + _Refresh() +EndFunc + +Func _FindWim() + ; Архив ищется ТОЛЬКО рядом с exe и только под именем Data.pf. + ; Параметр /wim= оставлен как служебный (автотесты), в обычной работе не нужен. + If $gWimPath <> "" Then + If FileExists($gWimPath) Then Return $gWimPath + _Msg(BitOR($MB_ICONWARNING, $MB_OK), "Архив не найден", "Файл не найден:" & @CRLF & $gWimPath) + EndIf + + Local $p = @ScriptDir & "\Data.pf" + If FileExists($p) Then Return $p + Return "" +EndFunc + +Func _PartitionLetter($disk, $idx) + ; Буква раздела с номером $idx (0..2) на диске $disk. Возвращает "X:\" или "". + Local $oWMI = ObjGet("winmgmts:\\.\root\cimv2") + If Not IsObj($oWMI) Then Return "" + Local $parts = $oWMI.ExecQuery("SELECT * FROM Win32_DiskPartition WHERE DiskIndex=" & $disk & " AND Index=" & $idx) + If Not IsObj($parts) Then Return "" + For $p In $parts + Local $ld = $oWMI.ExecQuery("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" & $p.DeviceID & "'} WHERE AssocClass=Win32_LogicalDiskToPartition") + If IsObj($ld) Then + For $l In $ld + Local $d = String($l.DeviceID) + If StringLen($d) = 2 Then Return $d & "\" + Next + EndIf + Next + Return "" +EndFunc + +Func _RunCmd($cmdline, $caption) + ; Запускает команду напрямую (без временных .cmd), показывает проценты, + ; возвращает код возврата: cmd печатает PF_EXIT=<код> после команды. + Local $full = @ComSpec & ' /v:on /c "' & $cmdline & ' & echo PF_EXIT=!errorlevel!"' + Local $pid = Run($full, "", @SW_HIDE, 2 + 4) + If $pid = 0 Then Return -1 + $gRunPid = $pid + + Local $out = "", $buf = "", $shown = -1 + While 1 + $buf = StdoutRead($pid) + If @error Then ExitLoop + If $buf <> "" Then + $out = $out & $buf + Local $m = StringRegExp($out, "(\d{1,3}(?:[.,]\d+)?)\s*%", 3) + If IsArray($m) Then + Local $last = Number($m[UBound($m) - 1]) + If $last <> $shown Then + $shown = $last + GUICtrlSetData($hWork, $caption) + GUICtrlSetData($hProg, $last) + EndIf + EndIf + EndIf + If Not ProcessExists($pid) Then ExitLoop + _CancelPoll() + Sleep(50) + WEnd + $gRunPid = 0 + $out = $out & StdoutRead($pid) + $gLastOut = $out + + Local $code = -1 + Local $mm = StringRegExp($out, "PF_EXIT=(\d+)", 3) + If IsArray($mm) Then $code = Number($mm[UBound($mm) - 1]) + Return $code +EndFunc + +Func _DismApply($wim, $index, $dir, $caption) + ; Прямая распаковка образа на раздел через DISM (Windows 8 и новее). + Local $cmd = 'dism.exe /Apply-Image /ImageFile:"' & $wim & '" /Index:' & $index & ' /ApplyDir:' & $dir + Local $rc = _RunCmd($cmd, $caption) + If $rc = 0 Then Return True + + GUICtrlSetData($hWork, $caption & ": ОШИБКА (код " & $rc & ")") + Return False +EndFunc + +Func _HideVentoy($disk) + ; Убираем букву у раздела VTOYEFI (2-й раздел) — в проводнике он не появится. + ; Работает по номеру диска и раздела, без привязки к буквам. + Local $tmp = @TempDir & "\paravis_" & $gRun & "_hide.txt" + Local $h = FileOpen($tmp, $FO_OVERWRITE + $FO_ANSI) + If $h = -1 Then Return + FileWrite($h, "select disk " & $disk & @CRLF & _ + "select partition 2" & @CRLF & _ + "remove" & @CRLF & _ + "set id=EF override" & @CRLF & _ + "exit" & @CRLF) + FileClose($h) + RunWait(@ComSpec & ' /c diskpart.exe /s "' & $tmp & '" > nul 2>&1', "", @SW_HIDE) + FileDelete($tmp) +EndFunc +Func _ApplyImages($disk, $wim) + ; 1 -> PARAVIS, 2 -> VTOYEFI, 3 -> DATA + Local $names[3] = ["PARAVIS", "VTOYEFI", "DATA"] + Local $i + For $i = 0 To 2 + If $gCancel Then Return 3 + ; буква может появиться не мгновенно после assign — ждём до 15 с + Local $dir = "" + Local $try + For $try = 1 To 15 + $dir = _PartitionLetter($disk, $i) + If $dir <> "" Then ExitLoop + Sleep(1000) + Next + If $dir = "" Then + _Msg(BitOR($MB_ICONERROR, $MB_OK), "Ошибка", _ + "Не удалось определить букву раздела " & ($i + 1) & " (" & $names[$i] & ")." & @CRLF & _ + "Проверьте, что раздел подключён (diskpart: assign).") + Return 2 + EndIf + If $gCancel Then Return 3 + Local $cap = $names[$i] & " → " & $dir + $gFailInfo = "образ " & ($i + 1) & " (" & $names[$i] & ") -> раздел " & $dir + GUICtrlSetData($hProg, 0) + GUICtrlSetState($hProg, $GUI_SHOW) + GUICtrlSetData($hWork, $cap) + Local $r + $r = _DismApply($wim, $i + 1, $dir, $cap) + If Not $r Then + If $gCancel Then Return 3 + Return 2 + EndIf + $gApplied = $gApplied + 1 + If $gCancel Then Return 3 + Next + Return 1 +EndFunc + +Func _VerifyLayout($disk, ByRef $okFlag) + ; Проверка результата по WMI (без разбора текста diskpart, не зависит от языка Windows). + $okFlag = False + Local $oWMI = ObjGet("winmgmts:\\.\root\cimv2") + If Not IsObj($oWMI) Then Return "разделов: ? (нет доступа к WMI)" + + Local $want = StringSplit("PARAVIS|VTOYEFI|DATA", "|", 2) + Local $rep = "" + Local $n = 0 + Local $labels = "|" + Local $sz1 = 0, $sz2 = 0, $sz3 = 0 + + Local $col = $oWMI.ExecQuery("SELECT * FROM Win32_DiskPartition WHERE DiskIndex=" & $disk) + If IsObj($col) Then + For $p In $col + $n = $n + 1 + Local $mbSz = Int(Number($p.Size) / $MB) + If $n = 1 Then $sz1 = $mbSz + If $n = 2 Then $sz2 = $mbSz + If $n = 3 Then $sz3 = $mbSz + Local $line = " раздел " & $n & ": " & $mbSz & " МБ" + Local $lds = $oWMI.ExecQuery("ASSOCIATORS OF {Win32_DiskPartition.DeviceID='" & $p.DeviceID & "'} WHERE AssocClass=Win32_LogicalDiskToPartition") + If IsObj($lds) Then + For $l In $lds + Local $vn = StringStripWS(String($l.VolumeName), 3) + $line = $line & " " & $l.DeviceID & " " & $vn & " " & String($l.FileSystem) + If $vn <> "" Then $labels = $labels & $vn & "|" + Next + EndIf + $rep = $rep & $line & @CRLF + Next + EndIf + + Local $miss = "" + For $w In $want + If StringInStr($labels, "|" & $w & "|") = 0 Then $miss = $miss & " " & $w + Next + + Local $sizeOk = (Abs($sz1 - $P1_MB) <= 8) And (Abs($sz2 - $P2_MB) <= 8) + + Local $head = "разделов: " & $n + If $n <> 3 Then $head = $head & " (ожидалось 3)" + If $miss <> "" Then $rep = $rep & "нет меток:" & $miss & @CRLF + If Not $sizeOk And $n = 3 Then $rep = $rep & "не совпали размеры 1-го и 2-го разделов" & @CRLF + + $okFlag = ($n = 3) And ($miss = "") And $sizeOk + Return $head & @CRLF & $rep +EndFunc + + +Func _BtnPoll() + Static $goCur = -1, $comboCur = -1 + If $gModal Then Return + If $gBusy Then + _CancelPoll() + Return + EndIf + Local $ci = GUIGetCursorInfo($hGUI) + If Not IsArray($ci) Then Return + + ; главная кнопка — клик через опрос + If _GoPoll() Then + _Apply() + Return + EndIf + + ; --- кнопки шапки: подсветка, нажатие, перетаскивание окна + Static $minCur = -1, $closeCur = -1, $minDown = False, $closeDown = False + Static $drag = False, $dragX = 0, $dragY = 0 + Local $minBg = $CLR_HEAD, $closeBg = $CLR_HEAD + + If $ci[4] = $hMin Then + If $ci[2] = 1 Then + $minBg = 0x3A4A6B + Else + $minBg = 0x2C3A57 + EndIf + EndIf + If $ci[4] = $hCloseBtn Then + If $ci[2] = 1 Then + $closeBg = 0xC0392B + Else + $closeBg = 0xE04B3F + EndIf + EndIf + If $minBg <> $minCur Then + GUICtrlSetBkColor($hMin, $minBg) + $minCur = $minBg + EndIf + If $closeBg <> $closeCur Then + GUICtrlSetBkColor($hCloseBtn, $closeBg) + $closeCur = $closeBg + EndIf + + ; нажатие засчитывается, если отпустили на той же кнопке + If $ci[4] = $hMin And $ci[2] = 1 Then + $minDown = True + ElseIf $minDown Then + $minDown = False + If $ci[4] = $hMin And $ci[2] = 0 Then GUISetState(@SW_MINIMIZE, $hGUI) + EndIf + If $ci[4] = $hCloseBtn And $ci[2] = 1 Then + $closeDown = True + ElseIf $closeDown Then + $closeDown = False + If $ci[4] = $hCloseBtn And $ci[2] = 0 Then Exit + EndIf + + ; перетаскивание за шапку (кнопки шапки исключены) + If ($ci[4] = $hHead Or $ci[4] = $hTitle Or $ci[4] = $hSub) And $ci[2] = 1 And Not $drag Then + Local $m = MouseGetPos() + Local $wp = WinGetPos($hGUI) + If IsArray($m) And IsArray($wp) Then + $drag = True + $dragX = $m[0] - $wp[0] + $dragY = $m[1] - $wp[1] + EndIf + EndIf + If $drag Then + If $ci[2] = 0 Then + $drag = False + Else + Local $m2 = MouseGetPos() + If IsArray($m2) Then WinMove($hGUI, "", $m2[0] - $dragX, $m2[1] - $dragY) + EndIf + EndIf + + ; главная кнопка + Local $go + If (Not $gCanInstall) Or BitAND(GUICtrlGetState($hGo), $GUI_DISABLE) Then + $go = 0xAEB6C4 + ElseIf $ci[4] = $hGo And $ci[2] = 1 Then + $go = 0x1B4FD8 + ElseIf $ci[4] = $hGo Then + $go = 0x3B82F6 + Else + $go = 0x2563EB + EndIf + If $go <> $goCur Then + GUICtrlSetBkColor($hGo, $go) + $goCur = $go + EndIf + + ; поле списка: раскрытие по нажатию (события надписи не всегда приходят) + Static $dropDown = False + Local $hasDisks = (UBound($aDiskName) > 0) + If $hasDisks And ($ci[4] = $hDrop Or $ci[4] = $hArrow Or $ci[4] = $hDropName Or $ci[4] = $hDropSize) And $ci[2] = 1 And Not $dropDown Then + _DropToggle() + $dropDown = True + EndIf + If $ci[2] = 0 Then $dropDown = False + + ; поле списка: подсветка рамки + Local $cb + If $hasDisks And ($ci[4] = $hDrop Or $ci[4] = $hArrow Or $ci[4] = $hComboFrame Or $ci[4] = $hDropName Or $ci[4] = $hDropSize) Then + $cb = 0x7FA6F0 + Else + $cb = 0xD5DAE3 + EndIf + If $cb <> $comboCur Then + GUICtrlSetBkColor($hComboFrame, $cb) + $comboCur = $cb + EndIf + + ; раскрытый список: подсветка пункта, выбор и закрытие по клику вне + If $hPopup <> 0 Then + Local $ci2 = GUIGetCursorInfo($hPopup) + If IsArray($ci2) Then + Local $k, $bg, $over = -1 + For $k = 0 To UBound($hItemName) - 1 + $bg = 0xFFFFFF + If $ci2[4] = $hItemName[$k] Or $ci2[4] = $hItemCap[$k] Then + $bg = 0xEEF3FF + $over = $k + EndIf + If $k < UBound($aItemBg) And $aItemBg[$k] <> $bg Then + GUICtrlSetBkColor($hItemName[$k], $bg) + GUICtrlSetBkColor($hItemCap[$k], $bg) + $aItemBg[$k] = $bg + EndIf + Next + + If $ci2[2] = 1 And TimerDiff($gDropGuard) > 400 Then + If $over >= 0 Then + ; выбрали пункт + $gSelIdx = $over + _ShowSelected() + _RefreshGo() + _DropClose() + ElseIf $ci2[4] = 0 Then + ; клик мимо — просто закрыть + _DropClose() + EndIf + EndIf + EndIf + EndIf +EndFunc +Func _DimControls($on) + ; Затеняем поле выбора и кнопку «Обновить», когда интерфейс недоступен + If $on Then + GUICtrlSetBkColor($hDrop, 0xF2F3F5) + GUICtrlSetBkColor($hDropName, 0xF2F3F5) + GUICtrlSetColor($hDropName, 0xA0A6B1) + GUICtrlSetBkColor($hDropSize, 0xF2F3F5) + GUICtrlSetColor($hDropSize, 0xB4B9C2) + GUICtrlSetColor($hArrow, 0xC7CBD3) + GUICtrlSetBkColor($hArrow, 0xF2F3F5) + GUICtrlSetBkColor($hComboFrame, 0xE3E6EB) + _DropClose() + Else + GUICtrlSetBkColor($hDrop, 0xFFFFFF) + GUICtrlSetBkColor($hDropName, 0xFFFFFF) + GUICtrlSetColor($hDropName, 0x1F2430) + GUICtrlSetBkColor($hDropSize, 0xFFFFFF) + GUICtrlSetColor($hDropSize, 0x6B7280) + GUICtrlSetBkColor($hArrow, 0xFFFFFF) + _ArrowState() + GUICtrlSetBkColor($hComboFrame, 0xD5DAE3) + EndIf +EndFunc + +Func _BusySet($b) + ; Идёт установка: предупреждение скрыто, показан ход работ, всё затенено, + ; кнопка превращается в «ОТМЕНИТЬ» + $gBusy = $b + If $b Then + $gCancel = False + $gCancelDown = False + $gCancelBg = -1 + $gApplied = 0 + GUICtrlSetState($hHint, $GUI_HIDE) + GUICtrlSetState($hWork, $GUI_SHOW) + _DimControls(True) + GUICtrlSetData($hGo, "ОТМЕНИТЬ") + GUICtrlSetColor($hGo, 0xFFFFFF) + _CancelLook() + Else + $gCancel = False + $gCancelDown = False + $gPhase = 0 + $gRunPid = 0 + _CheckInstalled() + _DimControls(False) + _RefreshGo() + EndIf +EndFunc + +Func _CancelLook() + ; Кнопка отмены: красная (под курсором светлее, нажата темнее), + ; после нажатия — серая «Отменяю. Ждите...» + Local $bg, $fg + If $gCancel Then + $bg = 0xAEB6C4 + $fg = 0xEDEFF3 + Else + $bg = 0xC0392B + $fg = 0xFFFFFF + Local $ci = GUIGetCursorInfo($hGUI) + If IsArray($ci) And $ci[4] = $hGo Then + If $ci[2] = 1 Then + $bg = 0xA93226 + Else + $bg = 0xD6453C + EndIf + EndIf + EndIf + If $bg <> $gCancelBg Then + GUICtrlSetBkColor($hGo, $bg) + GUICtrlSetColor($hGo, $fg) + $gCancelBg = $bg + EndIf +EndFunc + +Func _GoPoll() + ; Клик по главной кнопке через опрос курсора (события AutoIt копятся и срабатывают позже) + Static $down = False + Local $ci = GUIGetCursorInfo($hGUI) + If Not IsArray($ci) Then Return False + If $ci[4] = $hGo And $ci[2] = 1 Then + $down = True + Return False + EndIf + If $down Then + $down = False + If $ci[4] = $hGo And $ci[2] = 0 Then + If TimerDiff($gClickGuard) < 400 Then Return False + Return True + EndIf + EndIf + Return False +EndFunc + +Func _RequestCancel() + ; Нажали «ОТМЕНИТЬ»: кнопка сразу серая и больше не реагирует на клики. + ; Разметку доигрываем до конца, копирование прерываем сразу. + If $gCancel Then Return + $gCancel = True + $gCancelDown = False + GUICtrlSetData($hGo, "Отменяю. Ждите...") + GUICtrlSetBkColor($hGo, 0xAEB6C4) + GUICtrlSetColor($hGo, 0xEDEFF3) + $gCancelBg = 0xAEB6C4 + If $gPhase = 2 Then _KillCopy() +EndFunc + +Func _KillCopy() + ; Прерываем текущую распаковку: гасим всё дерево процессов cmd/dism + If $gRunPid <> 0 Then + Run(@ComSpec & ' /c taskkill /T /F /PID ' & $gRunPid, "", @SW_HIDE) + $gRunPid = 0 + EndIf +EndFunc + +Func _CancelPoll() + ; Нажатие «ОТМЕНИТЬ» во время установки. + ; Разметку не прерываем — ждём её конца. Копирование прерываем сразу. + If Not $gBusy Then Return + If $gCancel Then Return + _CancelLook() + If _GoPoll() Then _RequestCancel() +EndFunc +Func _Msg($flags, $title, $text) + ; Пока открыто окно — интерфейс затенён и неактивен + Local $prev = $gModal + $gModal = True + _GoState(False) + _DimControls(True) + Local $r = MsgBox($flags, $title, $text) + $gModal = $prev + $gClickGuard = TimerInit() + If Not $gBusy Then + _DimControls(False) + _RefreshGo() + Else + _CancelLook() + EndIf + Return $r +EndFunc + +Func _GoState($on) + ; Цвет главной кнопки: активна / недоступна (идёт запись или открыт диалог) + If $on Then + GUICtrlSetBkColor($hGo, 0x2563EB) + GUICtrlSetColor($hGo, 0xFFFFFF) + Else + GUICtrlSetBkColor($hGo, 0xAEB6C4) + GUICtrlSetColor($hGo, 0xEDEFF3) + EndIf +EndFunc +Func _ArrowState() + ; Треугольник списка: тусклый, если накопителей нет + If UBound($aDiskName) = 0 Then + GUICtrlSetColor($hArrow, 0xC7CBD3) + Else + GUICtrlSetColor($hArrow, 0x6B7280) + EndIf +EndFunc + +Func _GoCaption() + ; Надпись на главной кнопке: сборка уже стоит — «Переустановить» + If $gBusy Then Return + If $gInstalled Then + GUICtrlSetData($hGo, "Переустановить") + Else + GUICtrlSetData($hGo, "УСТАНОВИТЬ") + EndIf +EndFunc + +Func _CheckInstalled() + ; Сборка считается установленной, если на выбранном диске есть оба маркера: + ; 1-й раздел: \efi\Paravis\Boot\NOT.DEL, 3-й раздел: \ISO\Ventoy\NOT.DEL + $gInstalled = False + If UBound($aDiskNum) > 0 And $gSelIdx >= 0 And $gSelIdx < UBound($aDiskNum) Then + Local $disk = $aDiskNum[$gSelIdx] + Local $p1 = _PartitionLetter($disk, 0) + If $p1 <> "" And FileExists($p1 & "efi\Paravis\Boot\NOT.DEL") Then + Local $p3 = _PartitionLetter($disk, 2) + If $p3 <> "" And FileExists($p3 & "ISO\Ventoy\NOT.DEL") Then $gInstalled = True + EndIf + EndIf + _GoCaption() +EndFunc +Func _CanInstall() + If UBound($aDiskNum) = 0 Then Return False + If $gSelIdx < 0 Or $gSelIdx >= UBound($aDiskSize) Then Return False + ; порог: заявленный объём меньше 32 ГБ — установка запрещена + If _NominalGb($aDiskSize[$gSelIdx]) < 32 Then Return False + Return True +EndFunc + +Func _RefreshGo() + $gCanInstall = _CanInstall() + _GoCaption() + _GoState($gCanInstall) +EndFunc \ No newline at end of file diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..6d6cdd1 --- /dev/null +++ b/src/app.h @@ -0,0 +1,151 @@ +#pragma once + +#include "disks.h" +#include "util.h" + +#include +#include +#include + +enum class Hit { + None, + Header, + Title, + Sub, + Min, + Close, + Card, + Label, + Combo, + Drop, + DropName, + DropSize, + Arrow, + Status, + Progress, + Work, + Go, + Hint +}; + +class App { +public: + HINSTANCE hInst = nullptr; + HWND hwnd = nullptr; + HWND hProgress = nullptr; + HWND hPopup = nullptr; + UINT dpi = 96; + + HFONT fontTitle = nullptr; + HFONT fontSub = nullptr; + HFONT fontMin = nullptr; + HFONT fontClose = nullptr; + HFONT fontText = nullptr; + HFONT fontBold = nullptr; + HFONT fontGo = nullptr; + HFONT fontArrow = nullptr; + + int width = 376; + int height = 183; + + std::vector disks; + int selIdx = 0; + bool busy = false; + bool modal = false; + bool canInstall = false; + bool installed = false; + bool cancel = false; + bool needScan = false; + bool dimmed = false; + int phase = 0; + int applied = 0; + + ULONGLONG devTick = 0; + ULONGLONG clickGuard = 0; + ULONGLONG dropGuard = 0; + bool goPollDown = false; + bool dropDownGui = false; + bool popupMouseDown = false; + bool minDown = false; + bool closeDown = false; + bool dragging = false; + int dragX = 0; + int dragY = 0; + + std::wstring sig = L"-"; + std::wstring runStamp; + std::wstring wimPath; + int testDisk = -1; + std::wstring workText; + std::wstring labelText = L"Выбран накопитель:"; + std::wstring statusText; + std::wstring goText = L"УСТАНОВИТЬ"; + + COLORREF goBg = ColorFromHex(0x2563EB); + COLORREF goFg = RGB(255, 255, 255); + COLORREF comboFrame = ColorFromHex(0xD5DAE3); + COLORREF minBg = ColorFromHex(0x1E2A44); + COLORREF closeBg = ColorFromHex(0x1E2A44); + + COLORREF dropBg = RGB(255, 255, 255); + COLORREF nameBg = RGB(255, 255, 255); + COLORREF nameFg = ColorFromHex(0x1F2430); + COLORREF sizeBg = RGB(255, 255, 255); + COLORREF sizeFg = ColorFromHex(0x1F2430); + COLORREF arrowBg = RGB(255, 255, 255); + COLORREF arrowFg = ColorFromHex(0x6B7280); + + int popupHover = -1; + + HANDLE runJob = nullptr; + HANDLE runProcess = nullptr; + DWORD runPid = 0; + + bool Init(HWND window); + void CreateFonts(); + void DestroyFonts(); + + void ScanDisks(); + void ShowSelected(); + void RefreshGo(); + void GoCaption(); + void GoState(bool on); + void ArrowState(); + void DimControls(bool on); + void BusySet(bool value); + void CancelLook(Hit hit, bool leftDown); + void CheckInstalled(); + void Apply(); + int ApplyImages(int disk, const std::wstring& wim); + bool DismApply(const std::wstring& wim, int index, const std::wstring& dir, + const std::wstring& caption); + int RunCmd(const std::wstring& commandLine, const std::wstring& caption); + void HideVentoy(int disk); + std::wstring FindWim(); + int ShowMessage(UINT flags, const wchar_t* title, const std::wstring& text); + + void SetWork(const std::wstring& text); + void ShowProgress(int value); + void HideProgress(); + void InvalidateAll(); + void InvalidatePopup(); + + void Paint(HDC dc); + void PaintPopup(HDC dc); + void OpenPopup(); + void ClosePopup(); + void TogglePopup(); + void HandlePopup(POINT screen, bool leftDown); + void OnPoll(); + void OnAutoRefresh(); + void OnDeviceChange(WPARAM wParam); + void PumpMessages(); + void SleepPump(int milliseconds); + bool WaitForProcess(HANDLE process); + bool GoPoll(Hit hit, bool leftDown); + void ConfirmCancel(); + void RequestCancel(); + + Hit HitTest(POINT pt) const; + RECT PopupScreenRect() const; +}; diff --git a/src/app.manifest b/src/app.manifest new file mode 100644 index 0000000..60082e6 --- /dev/null +++ b/src/app.manifest @@ -0,0 +1,22 @@ + + + + PARAVIS FLASH installer + + + + + + + + + + + + + + + true + + + diff --git a/src/app.rc b/src/app.rc new file mode 100644 index 0000000..f0ab242 --- /dev/null +++ b/src/app.rc @@ -0,0 +1,32 @@ +#include + +1 ICON "sec.ico" +1 24 "app.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +PRODUCTVERSION 1,0,0,0 +FILEFLAGSMASK 0x3fL +FILEFLAGS 0x0L +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "paravis.ru" + VALUE "FileDescription", "PARAVIS FLASH installer" + VALUE "FileVersion", "1.0.0.0" + VALUE "InternalName", "ParavisInstaller" + VALUE "OriginalFilename", "Paravis installer.exe" + VALUE "ProductName", "PARAVIS FLASH" + VALUE "ProductVersion", "1.0.0.0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/src/disks.cpp b/src/disks.cpp new file mode 100644 index 0000000..4bd2043 --- /dev/null +++ b/src/disks.cpp @@ -0,0 +1,496 @@ +#include "disks.h" +#include "util.h" + +#include + +#include +#include +#include +#include + +namespace { + +HANDLE OpenPhysicalDrive(int disk) +{ + std::wstring path = L"\\\\.\\PhysicalDrive" + std::to_wstring(disk); + HANDLE handle = CreateFileW(path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + handle = CreateFileW(path.c_str(), 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, 0, nullptr); + } + return handle; +} + +HANDLE OpenVolume(const wchar_t letter) +{ + std::wstring path = L"\\\\.\\"; + path.push_back(letter); + path.push_back(L':'); + HANDLE handle = CreateFileW(path.c_str(), 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + handle = CreateFileW(path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, 0, nullptr); + } + return handle; +} + +std::wstring TrimAscii(std::string value) +{ + size_t begin = 0; + while (begin < value.size() && (value[begin] == ' ' || value[begin] == '\t' || + value[begin] == '\r' || value[begin] == '\n')) { + ++begin; + } + size_t end = value.size(); + while (end > begin && (value[end - 1] == ' ' || value[end - 1] == '\t' || + value[end - 1] == '\r' || value[end - 1] == '\n')) { + --end; + } + return AnsiToWide(value.substr(begin, end - begin), CP_ACP); +} + +std::wstring TrimWide(std::wstring value) +{ + size_t begin = 0; + while (begin < value.size() && iswspace(value[begin])) { + ++begin; + } + size_t end = value.size(); + while (end > begin && iswspace(value[end - 1])) { + --end; + } + return value.substr(begin, end - begin); +} + +std::wstring DescriptorString(const std::vector& buffer, + const STORAGE_DEVICE_DESCRIPTOR& descriptor, + DWORD offset) +{ + if (offset == 0 || offset >= descriptor.Size || offset >= buffer.size()) { + return std::wstring(); + } + size_t available = std::min(descriptor.Size, buffer.size()) - offset; + const char* start = reinterpret_cast(buffer.data() + offset); + size_t length = 0; + while (length < available && start[length] != '\0') { + ++length; + } + return TrimAscii(std::string(start, length)); +} + +struct StorageDescriptor { + bool valid = false; + int busType = -1; + bool removable = false; + std::wstring vendor; + std::wstring product; +}; + +StorageDescriptor QueryStorageDescriptor(HANDLE handle) +{ + StorageDescriptor result; + STORAGE_PROPERTY_QUERY query{}; + query.PropertyId = StorageDeviceProperty; + query.QueryType = PropertyStandardQuery; + + std::vector buffer(4096); + DWORD returned = 0; + if (!DeviceIoControl(handle, IOCTL_STORAGE_QUERY_PROPERTY, &query, sizeof(query), + buffer.data(), static_cast(buffer.size()), &returned, nullptr)) { + return result; + } + if (returned < sizeof(STORAGE_DEVICE_DESCRIPTOR)) { + return result; + } + + auto* descriptor = reinterpret_cast(buffer.data()); + if (descriptor->Size < sizeof(STORAGE_DEVICE_DESCRIPTOR) || descriptor->Size > buffer.size()) { + return result; + } + + result.valid = true; + result.busType = static_cast(descriptor->BusType); + result.removable = descriptor->RemovableMedia != 0; + result.vendor = DescriptorString(buffer, *descriptor, descriptor->VendorIdOffset); + result.product = DescriptorString(buffer, *descriptor, descriptor->ProductIdOffset); + return result; +} + +bool QueryDiskSize(HANDLE handle, unsigned long long& size) +{ + GET_LENGTH_INFORMATION length{}; + DWORD returned = 0; + if (DeviceIoControl(handle, IOCTL_DISK_GET_LENGTH_INFO, nullptr, 0, + &length, sizeof(length), &returned, nullptr)) { + size = static_cast(length.Length.QuadPart); + return true; + } + + std::vector buffer(sizeof(DISK_GEOMETRY_EX) + 256); + if (DeviceIoControl(handle, IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, nullptr, 0, + buffer.data(), static_cast(buffer.size()), &returned, nullptr)) { + auto* geometry = reinterpret_cast(buffer.data()); + size = static_cast(geometry->DiskSize.QuadPart); + return true; + } + return false; +} + +std::wstring MakeDiskName(const StorageDescriptor& descriptor) +{ + std::wstring name; + if (!descriptor.vendor.empty()) { + name = descriptor.vendor; + } + if (!descriptor.product.empty()) { + if (!name.empty()) { + name += L" "; + } + name += descriptor.product; + } + name = TrimWide(name); + if (name.empty()) { + name = L"(без названия)"; + } + return name; +} + +bool AddDisk(std::vector& disks, int number, + const StorageDescriptor& descriptor, unsigned long long size) +{ + DiskItem item; + item.number = number; + item.size = size; + item.name = MakeDiskName(descriptor); + item.cap = NominalSize(size); + disks.push_back(std::move(item)); + return true; +} + +bool GetVolumeExtents(wchar_t letter, + std::vector>& extents) +{ + HANDLE handle = OpenVolume(letter); + if (handle == INVALID_HANDLE_VALUE) { + return false; + } + + std::vector buffer(sizeof(VOLUME_DISK_EXTENTS) + 32 * sizeof(DISK_EXTENT)); + DWORD returned = 0; + bool ok = DeviceIoControl(handle, IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS, nullptr, 0, + buffer.data(), static_cast(buffer.size()), + &returned, nullptr) != FALSE; + CloseHandle(handle); + if (!ok || returned < sizeof(VOLUME_DISK_EXTENTS)) { + return false; + } + + auto* info = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < info->NumberOfDiskExtents; ++i) { + extents.emplace_back(static_cast(info->Extents[i].DiskNumber), + static_cast(info->Extents[i].StartingOffset.QuadPart)); + } + return !extents.empty(); +} + +bool FindVolumeForPartition(int disk, unsigned long long offset, + wchar_t& letter, std::wstring& label, std::wstring& fileSystem) +{ + for (wchar_t ch = L'A'; ch <= L'Z'; ++ch) { + std::wstring root; + root.push_back(ch); + root += L":\\"; + if (GetDriveTypeW(root.c_str()) == DRIVE_NO_ROOT_DIR) { + continue; + } + + std::vector> extents; + if (!GetVolumeExtents(ch, extents)) { + continue; + } + + bool matched = false; + for (const auto& extent : extents) { + if (extent.first == disk && extent.second == offset) { + matched = true; + break; + } + } + if (!matched) { + continue; + } + + letter = ch; + wchar_t volumeName[256] = {}; + wchar_t fsName[64] = {}; + DWORD serial = 0; + DWORD maxComponent = 0; + DWORD flags = 0; + if (GetVolumeInformationW(root.c_str(), volumeName, 256, &serial, + &maxComponent, &flags, fsName, 64)) { + label = TrimWide(volumeName); + fileSystem = fsName; + } + return true; + } + return false; +} + +} // namespace + +std::vector ScanUsbDisks() +{ + std::vector disks; + struct Candidate { + int number; + bool removable; + unsigned long long size; + StorageDescriptor descriptor; + }; + std::vector candidates; + + for (int disk = 0; disk < 128; ++disk) { + HANDLE handle = OpenPhysicalDrive(disk); + if (handle == INVALID_HANDLE_VALUE) { + continue; + } + + StorageDescriptor descriptor = QueryStorageDescriptor(handle); + unsigned long long size = 0; + QueryDiskSize(handle, size); + CloseHandle(handle); + + if (descriptor.valid && descriptor.busType == BusTypeUsb) { + AddDisk(disks, disk, descriptor, size); + } else { + candidates.push_back({disk, descriptor.valid && descriptor.removable, size, descriptor}); + } + } + + if (disks.empty()) { + for (const auto& candidate : candidates) { + if (candidate.descriptor.valid && + (candidate.descriptor.busType == BusTypeUsb || candidate.removable)) { + AddDisk(disks, candidate.number, candidate.descriptor, candidate.size); + } + } + } + + return disks; +} + +bool GetDiskSizeBytes(int disk, unsigned long long& size) +{ + HANDLE handle = OpenPhysicalDrive(disk); + if (handle == INVALID_HANDLE_VALUE) { + return false; + } + bool ok = QueryDiskSize(handle, size); + CloseHandle(handle); + return ok; +} + +std::vector GetSystemDiskIds() +{ + std::vector result; + wchar_t windowsDir[MAX_PATH] = {}; + if (GetWindowsDirectoryW(windowsDir, MAX_PATH) == 0) { + return result; + } + if (windowsDir[1] != L':') { + return result; + } + + wchar_t letter = static_cast(towupper(windowsDir[0])); + std::vector> extents; + if (!GetVolumeExtents(letter, extents)) { + return result; + } + + for (const auto& extent : extents) { + if (std::find(result.begin(), result.end(), extent.first) == result.end()) { + result.push_back(extent.first); + } + } + return result; +} + +double NominalGb(unsigned long long bytes) +{ + double gb = static_cast(bytes) / 1000000000.0; + static const int list[] = { + 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1000, 1500, 2000, 3000, + 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000 + }; + for (int value : list) { + if (gb <= static_cast(value) * 1.02) { + return static_cast(value); + } + } + return gb; +} + +std::wstring NominalSize(unsigned long long bytes) +{ + double value = NominalGb(bytes); + if (value >= 1000.0) { + return Format(L"%g ТБ", value / 1000.0); + } + return Format(L"%d ГБ", static_cast(value)); +} + +std::vector GetPartitions(int disk, bool& ok) +{ + ok = false; + std::vector result; + + HANDLE handle = OpenPhysicalDrive(disk); + if (handle == INVALID_HANDLE_VALUE) { + return result; + } + + std::vector buffer(sizeof(DRIVE_LAYOUT_INFORMATION_EX) + + 256 * sizeof(PARTITION_INFORMATION_EX)); + DWORD returned = 0; + bool success = DeviceIoControl(handle, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, nullptr, 0, + buffer.data(), static_cast(buffer.size()), + &returned, nullptr) != FALSE; + if (!success && GetLastError() == ERROR_INSUFFICIENT_BUFFER && returned > buffer.size()) { + buffer.resize(returned); + success = DeviceIoControl(handle, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, nullptr, 0, + buffer.data(), static_cast(buffer.size()), + &returned, nullptr) != FALSE; + } + CloseHandle(handle); + + if (!success || returned < sizeof(DRIVE_LAYOUT_INFORMATION_EX)) { + return result; + } + + auto* layout = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < layout->PartitionCount; ++i) { + const PARTITION_INFORMATION_EX& entry = layout->PartitionEntry[i]; + if (entry.PartitionLength.QuadPart <= 0) { + continue; + } + PartitionInfo info; + info.offset = static_cast(entry.StartingOffset.QuadPart); + info.size = static_cast(entry.PartitionLength.QuadPart); + info.number = static_cast(entry.PartitionNumber); + result.push_back(info); + } + + std::sort(result.begin(), result.end(), [](const PartitionInfo& a, const PartitionInfo& b) { + return a.offset < b.offset; + }); + ok = true; + return result; +} + +std::wstring GetPartitionLetter(int disk, int idx) +{ + bool ok = false; + std::vector partitions = GetPartitions(disk, ok); + if (!ok || idx < 0 || idx >= static_cast(partitions.size())) { + return std::wstring(); + } + + wchar_t letter = 0; + std::wstring label; + std::wstring fileSystem; + if (!FindVolumeForPartition(disk, partitions[idx].offset, letter, label, fileSystem)) { + return std::wstring(); + } + + std::wstring result; + result.push_back(letter); + result += L":\\"; + return result; +} + +bool DiskIsInstalled(int disk) +{ + std::wstring first = GetPartitionLetter(disk, 0); + if (first.empty() || !FileExists(first + L"efi\\Paravis\\Boot\\NOT.DEL")) { + return false; + } + std::wstring third = GetPartitionLetter(disk, 2); + if (third.empty() || !FileExists(third + L"ISO\\Ventoy\\NOT.DEL")) { + return false; + } + return true; +} + +std::wstring VerifyLayout(int disk, bool& ok) +{ + ok = false; + bool layoutOk = false; + std::vector partitions = GetPartitions(disk, layoutOk); + if (!layoutOk) { + return L"разделов: ? (нет доступа к WMI)"; + } + + const wchar_t* want[] = { L"PARAVIS", L"VTOYEFI", L"DATA" }; + std::wstring report; + std::wstring labels = L"|"; + unsigned long long sizes[3] = { 0, 0, 0 }; + int count = static_cast(partitions.size()); + + for (int i = 0; i < count; ++i) { + unsigned long long megabytes = partitions[i].size / 1048576ULL; + if (i < 3) { + sizes[i] = megabytes; + } + std::wstring line = Format(L" раздел %d: %llu МБ", i + 1, megabytes); + + wchar_t letter = 0; + std::wstring label; + std::wstring fileSystem; + if (FindVolumeForPartition(disk, partitions[i].offset, letter, label, fileSystem)) { + std::wstring device; + device.push_back(letter); + device += L":"; + line += L" " + device + L" " + label + L" " + fileSystem; + if (!label.empty()) { + labels += label + L"|"; + } + } + report += line + L"\r\n"; + } + + std::wstring missing; + for (const wchar_t* name : want) { + std::wstring token = std::wstring(L"|") + name + L"|"; + if (labels.find(token) == std::wstring::npos) { + missing += L" "; + missing += name; + } + } + + auto absDiff = [](unsigned long long value, unsigned long long expected) -> long long { + return value >= expected + ? static_cast(value - expected) + : static_cast(expected - value); + }; + bool sizeOk = absDiff(sizes[0], 16384) <= 8 && absDiff(sizes[1], 32) <= 8; + + std::wstring head = Format(L"разделов: %d", count); + if (count != 3) { + head += L" (ожидалось 3)"; + } + if (!missing.empty()) { + report += L"нет меток:" + missing + L"\r\n"; + } + if (!sizeOk && count == 3) { + report += L"не совпали размеры 1-го и 2-го разделов\r\n"; + } + + ok = count == 3 && missing.empty() && sizeOk; + return head + L"\r\n" + report; +} diff --git a/src/disks.h b/src/disks.h new file mode 100644 index 0000000..ef6ca95 --- /dev/null +++ b/src/disks.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +struct DiskItem { + int number = 0; + unsigned long long size = 0; + std::wstring name; + std::wstring cap; +}; + +struct PartitionInfo { + unsigned long long offset = 0; + unsigned long long size = 0; + int number = 0; +}; + +std::vector ScanUsbDisks(); +bool GetDiskSizeBytes(int disk, unsigned long long& size); +std::vector GetSystemDiskIds(); +double NominalGb(unsigned long long bytes); +std::wstring NominalSize(unsigned long long bytes); +std::vector GetPartitions(int disk, bool& ok); +std::wstring GetPartitionLetter(int disk, int idx); +bool DiskIsInstalled(int disk); +std::wstring VerifyLayout(int disk, bool& ok); diff --git a/src/install.cpp b/src/install.cpp new file mode 100644 index 0000000..26a114e --- /dev/null +++ b/src/install.cpp @@ -0,0 +1,694 @@ +#include "app.h" +#include "disks.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +const unsigned long long MB = 1048576ULL; +const int P1_MB = 16384; +const int P2_MB = 32; +const int TAIL_MB = 3; + +std::wstring TempName(const std::wstring& run, const std::wstring& suffix) +{ + return GetTempDir() + L"paravis_" + run + L"_" + suffix; +} + +bool RunHidden(App* app, const std::wstring& commandLine) +{ + DebugLog(L"RunHidden: %s", commandLine.c_str()); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESHOWWINDOW; + startup.wShowWindow = SW_HIDE; + + PROCESS_INFORMATION process{}; + std::vector buffer(commandLine.begin(), commandLine.end()); + buffer.push_back(L'\0'); + + if (!CreateProcessW(nullptr, buffer.data(), nullptr, nullptr, FALSE, + CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process)) { + return false; + } + + app->WaitForProcess(process.hProcess); + DWORD exitCode = 1; + GetExitCodeProcess(process.hProcess, &exitCode); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + DebugLog(L"RunHidden done code=%lu", exitCode); + return exitCode == 0; +} + +void KillProcessTree(DWORD pid) +{ + if (pid == 0) { + return; + } + + wchar_t systemDir[MAX_PATH] = {}; + GetSystemDirectoryW(systemDir, MAX_PATH); + std::wstring command = L"\"" + std::wstring(systemDir) + + L"\\taskkill.exe\" /T /F /PID " + std::to_wstring(pid); + + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESHOWWINDOW; + startup.wShowWindow = SW_HIDE; + + PROCESS_INFORMATION process{}; + std::vector buffer(command.begin(), command.end()); + buffer.push_back(L'\0'); + if (CreateProcessW(nullptr, buffer.data(), nullptr, nullptr, FALSE, + CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process)) { + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + } +} + +bool LaunchLoader(const std::wstring& path) +{ + DebugLog(L"LaunchLoader: %s", path.c_str()); + std::wstring commandLine = L"\"" + path + L"\""; + std::vector buffer(commandLine.begin(), commandLine.end()); + buffer.push_back(L'\0'); + + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(nullptr, buffer.data(), nullptr, nullptr, FALSE, 0, + nullptr, nullptr, &startup, &process)) { + DebugLog(L"LaunchLoader failed error=%lu", GetLastError()); + return false; + } + DebugLog(L"LaunchLoader started pid=%lu", process.dwProcessId); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return true; +} + +void UpdateOutputProgress(App* app, const std::string& output, + const std::wstring& caption, double& shown) +{ + size_t percent = output.rfind('%'); + if (percent == std::string::npos) { + return; + } + + size_t index = percent; + while (index > 0 && (output[index - 1] == ' ' || output[index - 1] == '\t' || + output[index - 1] == '\r' || output[index - 1] == '\n')) { + --index; + } + size_t numberEnd = index; + while (index > 0 && (std::isdigit(static_cast(output[index - 1])) || + output[index - 1] == '.' || output[index - 1] == ',')) { + --index; + } + if (index == numberEnd) { + return; + } + + std::string number = output.substr(index, numberEnd - index); + std::replace(number.begin(), number.end(), ',', '.'); + char* parseEnd = nullptr; + double value = std::strtod(number.c_str(), &parseEnd); + if (parseEnd == number.c_str()) { + return; + } + + if (value != shown) { + shown = value; + app->SetWork(caption); + app->ShowProgress(static_cast(value)); + } +} + +} // namespace + +std::wstring App::FindWim() +{ + if (!wimPath.empty()) { + if (FileExists(wimPath)) { + return wimPath; + } + ShowMessage(MB_ICONWARNING | MB_OK, L"Архив не найден", + L"Файл не найден:\r\n" + wimPath); + } + + std::wstring path = JoinPath(GetExeDir(), L"Data.pf"); + if (FileExists(path)) { + return path; + } + return std::wstring(); +} + +void App::Apply() +{ + DebugLog(L"Apply enter busy=%d canInstall=%d testDisk=%d selIdx=%d disks=%zu", + busy ? 1 : 0, canInstall ? 1 : 0, testDisk, selIdx, disks.size()); + if (busy) { + return; + } + if (!canInstall && testDisk < 0) { + return; + } + + int disk = testDisk; + unsigned long long expectedSize = 0; + if (disk < 0) { + int index = (selIdx >= 0 && selIdx < static_cast(disks.size())) ? selIdx : -1; + if (index < 0) { + ShowMessage(MB_ICONWARNING | MB_OK, L"Устройство не выбрано", + L"Выберите флешку в списке."); + return; + } + disk = disks[index].number; + expectedSize = disks[index].size; + } + + std::vector systemDisks = GetSystemDiskIds(); + if (std::find(systemDisks.begin(), systemDisks.end(), disk) != systemDisks.end()) { + ShowMessage(MB_ICONERROR | MB_OK, L"Это системный диск", + Format(L"Диск %d содержит Windows. Разметка отменена - выберите другой диск.", + disk)); + return; + } + + unsigned long long sizeBytes = 0; + if (!GetDiskSizeBytes(disk, sizeBytes) || sizeBytes == 0) { + ShowMessage(MB_ICONERROR | MB_OK, L"Ошибка", + Format(L"Не удалось определить размер диска %d.", disk)); + return; + } + + if (expectedSize > 0) { + unsigned long long difference = sizeBytes > expectedSize + ? sizeBytes - expectedSize + : expectedSize - sizeBytes; + if (difference > expectedSize / 100) { + ShowMessage(MB_ICONERROR | MB_OK, L"Диск не совпадает", + Format(L"Размер выбранного диска (%.1f ГБ) не совпадает с тем, " + L"что был в списке (%.1f ГБ).\r\n\r\n" + L"Нажмите «Обновить» и выберите устройство заново.", + static_cast(sizeBytes) / 1073741824.0, + static_cast(expectedSize) / 1073741824.0)); + return; + } + } + + unsigned long long totalMB = sizeBytes / MB; + long long p3 = static_cast(totalMB) - 1 - P1_MB - P2_MB - TAIL_MB; + if (p3 < 512) { + ShowMessage(MB_ICONERROR | MB_OK, L"Слишком маленький диск", + Format(L"Нужен диск не меньше %d МБ.\r\nСейчас: %llu МБ.", + P1_MB + P2_MB + TAIL_MB + 512, totalMB)); + return; + } + + std::wstring message = + Format(L"Накопитель (%.1f ГБ)\r\n\r\n", + static_cast(sizeBytes) / 1073741824.0); + message += L"Будет создано:\r\n"; + message += L" 1) FAT32 16 ГБ метка PARAVIS\r\n"; + message += L" 2) FAT 32 МБ метка VTOYEFI\r\n"; + message += Format(L" 3) exFAT %.1f ГБ метка DATA\r\n\r\n", + static_cast(p3) / 1024.0); + message += L"Затем на разделы будут распакованы файлы из архива Data.pf\r\n"; + message += L"(1 -> PARAVIS, 2 -> VTOYEFI, 3 -> DATA).\r\n\r\n"; + message += L"ВНИМАНИЕ! ВСЕ ДАННЫЕ НА ЭТОМ ДИСКЕ БУДУТ УНИЧТОЖЕНЫ.\r\n"; + message += L"Продолжить?"; + + goText = L"Ожидание подтверждения..."; + goBg = ColorFromHex(0xAEB6C4); + goFg = ColorFromHex(0xEDEFF3); + InvalidateAll(); + + if (ShowMessage(MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2, + L"Подтверждение", message) != IDYES) { + GoCaption(); + RefreshGo(); + return; + } + + DebugLog(L"confirmation accepted, disk=%d size=%llu totalMB=%llu p3=%lld", + disk, sizeBytes, totalMB, p3); + BusySet(true); + phase = 1; + DebugLog(L"busy set, phase=1"); + + std::wstring onlineFile = TempName(runStamp, L"online.txt"); + std::wstring onlineScript = Format(L"select disk %d\r\nonline disk\r\nexit\r\n", disk); + if (WriteAnsiFile(onlineFile, onlineScript)) { + std::wstring command = L"\"" + GetComSpec() + L"\" /c diskpart.exe /s \"" + + onlineFile + L"\" > nul 2>&1"; + RunHidden(this, command); + DeleteFileW(onlineFile.c_str()); + } + + std::wstring script = Format( + L"select disk %d\r\n" + L"clean\r\n" + L"convert mbr\r\n" + L"create partition primary size=%d\r\n" + L"format fs=fat32 quick label=\"PARAVIS\"\r\n" + L"active\r\n" + L"assign\r\n" + L"create partition primary size=%d\r\n" + L"format fs=fat quick label=\"VTOYEFI\"\r\n" + L"assign\r\n" + L"create partition primary size=%lld\r\n" + L"format fs=exfat quick label=\"DATA\"\r\n" + L"assign\r\n" + L"exit\r\n", + disk, P1_MB, P2_MB, p3); + + std::wstring scriptFile = TempName(runStamp, L"diskpart.txt"); + std::wstring logFile = TempName(runStamp, L"diskpart_log.txt"); + if (!WriteAnsiFile(scriptFile, script)) { + ShowMessage(MB_ICONERROR | MB_OK, L"Ошибка", + L"Не удалось создать файл скрипта."); + BusySet(false); + HideProgress(); + return; + } + + SetWork(L"Разметка диска…"); + + std::wstring command = L"\"" + GetComSpec() + L"\" /c diskpart.exe /s \"" + + scriptFile + L"\" > \"" + logFile + L"\" 2>&1"; + DebugLog(L"diskpart start"); + RunHidden(this, command); + DebugLog(L"diskpart done"); + + std::string logBytes = ReadFileBytes(logFile); + std::wstring logText = AnsiToWide(logBytes, CP_ACP); + + SleepPump(1500); + + bool layoutOk = false; + std::wstring check = VerifyLayout(disk, layoutOk); + DebugLog(L"layoutOk=%d check=%s", layoutOk ? 1 : 0, check.c_str()); + + if (!layoutOk) { + SetWork(L"Раскладка не совпала с ожидаемой"); + HideProgress(); + ShowMessage(MB_ICONERROR | MB_OK, L"Ошибка разметки", + L"РАСКЛАДКА НЕ СОВПАЛА С ОЖИДАЕМОЙ\r\n\r\n" + check + + L"\r\n\r\nЛог: " + logFile + L"\r\n\r\n" + logText.substr(0, 600)); + BusySet(false); + HideProgress(); + return; + } + + if (cancel) { + BusySet(false); + HideProgress(); + ShowMessage(MB_ICONINFORMATION | MB_OK, L"Отменено", + L"Операция отменена пользователем.\r\n\r\n" + L"Диск размечен, файлы не скопированы."); + return; + } + + std::wstring wim = FindWim(); + DebugLog(L"wim=%s", wim.c_str()); + if (wim.empty()) { + SetWork(L"Размечено, архив Data.pf не найден"); + ShowMessage(MB_ICONWARNING | MB_OK, L"Разметка выполнена", + L"Диск размечен, раскладка верна.\r\n\r\n" + check + + L"\r\n\r\nНо файл Data.pf не найден рядом с программой - " + L"файлы не скопированы."); + BusySet(false); + HideProgress(); + return; + } + + phase = 2; + int result = ApplyImages(disk, wim); + phase = 0; + DebugLog(L"ApplyImages result=%d applied=%d cancel=%d", result, applied, cancel ? 1 : 0); + + if (result == 1) { + std::wstring first = GetPartitionLetter(disk, 0); + DebugLog(L"partition0=%s", first.c_str()); + + // If the C++ bootloader changer sits next to the installer, put it on + // the PARAVIS partition and use it instead of the copy from Data.pf. + if (!first.empty()) { + std::wstring source = JoinPath(GetExeDir(), L"Change_BootLoader.exe"); + if (FileExists(source)) { + std::wstring destination = first + L"Change_BootLoader.exe"; + if (_wcsicmp(source.c_str(), destination.c_str()) == 0) { + DebugLog(L"bootloader source is already the target: %s", source.c_str()); + } else if (CopyFileW(source.c_str(), destination.c_str(), FALSE)) { + DebugLog(L"copied bootloader: %s -> %s", source.c_str(), destination.c_str()); + } else { + DebugLog(L"bootloader copy failed error=%lu", GetLastError()); + } + } else { + DebugLog(L"no C++ bootloader next to installer: %s", source.c_str()); + } + } + + HideVentoy(disk); + SetWork(L"Готово"); + HideProgress(); + DebugLog(L"showing success message"); + + ShowMessage(MB_ICONINFORMATION | MB_OK, L"Установка завершена", + L"Установка завершена успешно. Сейчас будет запущен установщик загрузчиков. " + L"Выберите Legacy и UEFI загрузчики. Если этого не сделать, то будет " + L"работать только UEFI."); + + DebugLog(L"success message closed, partition0=%s", first.c_str()); + if (!first.empty()) { + std::wstring loader = first + L"Change_BootLoader.exe"; + DebugLog(L"loader path=%s exists=%d", loader.c_str(), FileExists(loader) ? 1 : 0); + if (FileExists(loader)) { + if (LaunchLoader(loader)) { + DebugLog(L"loader launched, ExitProcess"); + ExitProcess(0); + } + ShowMessage(MB_ICONWARNING | MB_OK, L"Установщик загрузчиков", + L"Не удалось запустить Change_BootLoader.exe.\r\n\r\n" + L"Путь: " + loader); + } else { + ShowMessage(MB_ICONWARNING | MB_OK, L"Установщик загрузчиков", + L"Файл Change_BootLoader.exe не найден на разделе PARAVIS.\r\n\r\n" + L"Путь: " + loader); + } + } else { + ShowMessage(MB_ICONWARNING | MB_OK, L"Установщик загрузчиков", + L"Не удалось определить букву раздела PARAVIS."); + } + } else if (result == 2) { + SetWork(L"Ошибка записи"); + HideProgress(); + ShowMessage(MB_ICONERROR | MB_OK, L"Ошибка копирования файлов", + L"Диск размечен, но скопировать файлы не удалось.\r\n\r\n" + L"Запустите установку ещё раз."); + } else if (result == 3) { + SetWork(L"Отменено"); + HideProgress(); + BusySet(false); + HideProgress(); + ShowMessage(MB_ICONINFORMATION | MB_OK, L"Отменено", + Format(L"Операция отменена пользователем.\r\n\r\n" + L"Диск размечен, скопировано образов: %d из 3.", applied)); + return; + } + + BusySet(false); + HideProgress(); +} + +int App::ApplyImages(int disk, const std::wstring& wim) +{ + const wchar_t* names[3] = { L"PARAVIS", L"VTOYEFI", L"DATA" }; + + for (int i = 0; i < 3; ++i) { + if (cancel) { + return 3; + } + + std::wstring dir; + for (int attempt = 0; attempt < 15; ++attempt) { + dir = GetPartitionLetter(disk, i); + if (!dir.empty()) { + break; + } + SleepPump(1000); + } + + if (dir.empty()) { + ShowMessage(MB_ICONERROR | MB_OK, L"Ошибка", + Format(L"Не удалось определить букву раздела %d (%s).\r\n" + L"Проверьте, что раздел подключён (diskpart: assign).", + i + 1, names[i])); + return 2; + } + + if (cancel) { + return 3; + } + + std::wstring caption = std::wstring(names[i]) + L" → " + dir; + DebugLog(L"ApplyImages image=%d part=%d dir=%s", i + 1, i, dir.c_str()); + ShowProgress(0); + SetWork(caption); + + if (!DismApply(wim, i + 1, dir, caption)) { + if (cancel) { + return 3; + } + return 2; + } + + ++applied; + + if (cancel) { + return 3; + } + } + + return 1; +} + +bool App::DismApply(const std::wstring& wim, int index, const std::wstring& dir, + const std::wstring& caption) +{ + std::wstring arguments = L"/Apply-Image /ImageFile:\"" + wim + + L"\" /Index:" + std::to_wstring(index) + + L" /ApplyDir:" + dir; + int code = RunCmd(arguments, caption); + if (code == 0) { + return true; + } + + SetWork(caption + Format(L": ОШИБКА (код %d)", code)); + return false; +} + +int App::RunCmd(const std::wstring& arguments, const std::wstring& caption) +{ + DebugLog(L"RunCmd start: dism %s", arguments.c_str()); + + wchar_t systemDir[MAX_PATH] = {}; + GetSystemDirectoryW(systemDir, MAX_PATH); + std::wstring dism = std::wstring(systemDir) + L"\\dism.exe"; + std::wstring full = L"\"" + dism + L"\" " + arguments; + + SECURITY_ATTRIBUTES security{}; + security.nLength = sizeof(security); + security.bInheritHandle = TRUE; + + HANDLE readPipe = nullptr; + HANDLE writePipe = nullptr; + if (!CreatePipe(&readPipe, &writePipe, &security, 0)) { + return -1; + } + SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0); + + HANDLE nullInput = CreateFileW(L"NUL", GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + &security, OPEN_EXISTING, 0, nullptr); + + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; + startup.wShowWindow = SW_HIDE; + startup.hStdOutput = writePipe; + startup.hStdError = writePipe; + startup.hStdInput = nullInput; + + PROCESS_INFORMATION process{}; + std::vector buffer(full.begin(), full.end()); + buffer.push_back(L'\0'); + + BOOL created = CreateProcessW(dism.c_str(), buffer.data(), nullptr, nullptr, TRUE, + CREATE_NO_WINDOW | CREATE_SUSPENDED, + nullptr, nullptr, &startup, &process); + CloseHandle(writePipe); + if (nullInput != INVALID_HANDLE_VALUE) { + CloseHandle(nullInput); + } + if (!created) { + CloseHandle(readPipe); + return -1; + } + + HANDLE job = CreateJobObjectW(nullptr, nullptr); + if (job) { + JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit{}; + limit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limit, sizeof(limit)); + if (!AssignProcessToJobObject(job, process.hProcess)) { + DebugLog(L"AssignProcessToJobObject failed error=%lu", GetLastError()); + } + } + + runJob = job; + runProcess = process.hProcess; + runPid = process.dwProcessId; + ResumeThread(process.hThread); + CloseHandle(process.hThread); + + std::string output; + double shown = -1.0; + bool killed = false; + + for (;;) { + for (;;) { + DWORD available = 0; + if (!PeekNamedPipe(readPipe, nullptr, 0, nullptr, &available, nullptr) || + available == 0) { + break; + } + char chunk[4096]; + DWORD toRead = std::min(available, sizeof(chunk)); + DWORD read = 0; + if (!ReadFile(readPipe, chunk, toRead, &read, nullptr) || read == 0) { + break; + } + output.append(chunk, read); + } + UpdateOutputProgress(this, output, caption, shown); + + DWORD wait = MsgWaitForMultipleObjects(1, &process.hProcess, FALSE, 30, QS_ALLINPUT); + if (wait == WAIT_OBJECT_0) { + for (;;) { + DWORD available = 0; + if (!PeekNamedPipe(readPipe, nullptr, 0, nullptr, &available, nullptr) || + available == 0) { + break; + } + char chunk[4096]; + DWORD toRead = std::min(available, sizeof(chunk)); + DWORD read = 0; + if (!ReadFile(readPipe, chunk, toRead, &read, nullptr) || read == 0) { + break; + } + output.append(chunk, read); + } + UpdateOutputProgress(this, output, caption, shown); + break; + } + if (wait != WAIT_OBJECT_0) { + // Pump even on timeout so due WM_TIMER messages run OnPoll and + // the cancel button stays responsive during DISM. + PumpMessages(); + } + + if (cancel && !killed) { + killed = true; + if (job) { + TerminateJobObject(job, 1); + } + TerminateProcess(process.hProcess, 1); + KillProcessTree(process.dwProcessId); + } + } + + WaitForSingleObject(process.hProcess, INFINITE); + + DWORD exitCode = 1; + GetExitCodeProcess(process.hProcess, &exitCode); + int code = static_cast(exitCode); + + runJob = nullptr; + runProcess = nullptr; + runPid = 0; + if (job) { + CloseHandle(job); + } + CloseHandle(process.hProcess); + CloseHandle(readPipe); + DebugLog(L"RunCmd done code=%d killed=%d cancel=%d", code, killed ? 1 : 0, cancel ? 1 : 0); + return code; +} + +void App::HideVentoy(int disk) +{ + std::wstring scriptFile = TempName(runStamp, L"hide.txt"); + std::wstring script = Format( + L"select disk %d\r\n" + L"select partition 2\r\n" + L"remove\r\n" + L"set id=EF override\r\n" + L"exit\r\n", disk); + + if (WriteAnsiFile(scriptFile, script)) { + std::wstring command = L"\"" + GetComSpec() + L"\" /c diskpart.exe /s \"" + + scriptFile + L"\" > nul 2>&1"; + RunHidden(this, command); + DeleteFileW(scriptFile.c_str()); + } +} + +int App::ShowMessage(UINT flags, const wchar_t* title, const std::wstring& text) +{ + DebugLog(L"MsgBox enter title=%s busy=%d", title, busy ? 1 : 0); + bool previous = modal; + modal = true; + GoState(false); + DimControls(true); + + int result = MessageBoxW(hwnd, text.c_str(), title, flags); + DebugLog(L"MsgBox exit title=%s result=%d", title, result); + + modal = previous; + clickGuard = GetTickCount64(); + if (!busy) { + DimControls(false); + RefreshGo(); + } else { + CancelLook(Hit::None, false); + } + InvalidateAll(); + return result; +} + +void App::ConfirmCancel() +{ + if (cancel) { + return; + } + DebugLog(L"ConfirmCancel phase=%d", phase); + int result = ShowMessage(MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2, + L"Отмена установки", + L"Вы уверены, что хотите отменить установку?"); + if (result == IDYES) { + RequestCancel(); + } +} + +void App::RequestCancel() +{ + DebugLog(L"RequestCancel phase=%d busy=%d", phase, busy ? 1 : 0); + if (cancel) { + return; + } + cancel = true; + goText = L"Отменяю. Ждите..."; + goBg = ColorFromHex(0xAEB6C4); + goFg = ColorFromHex(0xEDEFF3); + if (phase == 2) { + if (runJob) { + TerminateJobObject(runJob, 1); + } + if (runProcess) { + TerminateProcess(runProcess, 1); + } + // Fallback for environments where the process could not be assigned + // to a job: kill the whole DISM tree. + KillProcessTree(runPid); + } + InvalidateAll(); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..cfea4df --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,987 @@ +#include "app.h" +#include "disks.h" +#include "util.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +const wchar_t* kMainClass = L"ParavisInstallerCpp"; +const wchar_t* kPopupClass = L"ParavisInstallerCppPopup"; + +const DWORD CLR_BG = 0xF7F8FA; +const DWORD CLR_HEAD = 0x1E2A44; + +void FillRectColor(HDC dc, int x, int y, int w, int h, COLORREF color) +{ + RECT rect{ x, y, x + w, y + h }; + HBRUSH brush = CreateSolidBrush(color); + FillRect(dc, &rect, brush); + DeleteObject(brush); +} + +void DrawTextRect(HDC dc, const std::wstring& text, HFONT font, + int x, int y, int w, int h, COLORREF color, UINT flags) +{ + HGDIOBJ oldFont = SelectObject(dc, font); + int oldMode = SetBkMode(dc, TRANSPARENT); + COLORREF oldColor = SetTextColor(dc, color); + RECT rect{ x, y, x + w, y + h }; + DrawTextW(dc, text.c_str(), -1, &rect, flags | DT_NOPREFIX | DT_SINGLELINE); + SetTextColor(dc, oldColor); + SetBkMode(dc, oldMode); + SelectObject(dc, oldFont); +} + +std::wstring TempFile(const std::wstring& name) +{ + return GetTempDir() + name; +} + +void WriteSelftest() +{ + std::wstring text; + wchar_t windowsDir[MAX_PATH] = {}; + GetWindowsDirectoryW(windowsDir, MAX_PATH); + text += std::wstring(L"windows=") + windowsDir + L"\r\n"; + + std::wstring ids; + for (int disk : GetSystemDiskIds()) { + ids += L"|" + std::to_wstring(disk) + L"|"; + } + text += L"system_disks=" + ids + L"\r\n"; + text += std::wstring(L"admin=") + (IsAdmin() ? L"1" : L"0") + L"\r\n"; + + std::wstring wim = JoinPath(GetExeDir(), L"Data.pf"); + text += L"wim=" + wim + L"\r\n"; + text += std::wstring(L"wim_exists=") + (FileExists(wim) ? L"1" : L"0") + L"\r\n"; + + WriteAnsiFile(TempFile(L"paravis_selftest.txt"), text); +} + +void WriteVerify(int disk) +{ + bool ok = false; + std::wstring report = VerifyLayout(disk, ok); + std::wstring text = Format(L"disk=%d ok=%s\r\n", disk, ok ? L"True" : L"False"); + text += report; + text += L"\r\n"; + WriteAnsiFile(TempFile(L"paravis_verify.txt"), text); +} + +LRESULT CALLBACK PopupProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + App* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + + switch (message) { + case WM_CREATE: { + auto* create = reinterpret_cast(lParam); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, + reinterpret_cast(create->lpCreateParams)); + return 0; + } + case WM_ERASEBKGND: + return 1; + case WM_PAINT: { + PAINTSTRUCT paint{}; + HDC dc = BeginPaint(hwnd, &paint); + if (app) { + app->PaintPopup(dc); + } + EndPaint(hwnd, &paint); + return 0; + } + case WM_MOUSEACTIVATE: + return MA_NOACTIVATE; + case WM_DESTROY: + if (app && app->hPopup == hwnd) { + app->hPopup = nullptr; + } + return 0; + } + return DefWindowProcW(hwnd, message, wParam, lParam); +} + +LRESULT CALLBACK MainProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + App* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + + switch (message) { + case WM_CREATE: { + auto* create = reinterpret_cast(lParam); + app = reinterpret_cast(create->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + if (app) { + app->Init(hwnd); + } + return 0; + } + case WM_ERASEBKGND: + return 1; + case WM_PAINT: { + PAINTSTRUCT paint{}; + HDC dc = BeginPaint(hwnd, &paint); + if (app) { + app->Paint(dc); + } + EndPaint(hwnd, &paint); + return 0; + } + case WM_TIMER: + if (app) { + if (wParam == 1) { + app->OnPoll(); + } else if (wParam == 2) { + app->OnAutoRefresh(); + } + } + return 0; + case WM_DEVICECHANGE: + if (app) { + app->OnDeviceChange(wParam); + } + return TRUE; + case WM_CLOSE: + DebugLog(L"WM_CLOSE busy=%d", (app && app->busy) ? 1 : 0); + if (app && app->busy) { + // Closing during installation must not leave diskpart/DISM running + // in the background. Treat it like the cancel button. + app->ConfirmCancel(); + return 0; + } + DestroyWindow(hwnd); + return 0; + case WM_DESTROY: + DebugLog(L"WM_DESTROY"); + if (app) { + if (app->hPopup) { + DestroyWindow(app->hPopup); + app->hPopup = nullptr; + } + app->DestroyFonts(); + } + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(hwnd, message, wParam, lParam); +} + +} // namespace + +bool App::Init(HWND window) +{ + hwnd = window; + HDC dc = GetDC(hwnd); + dpi = static_cast(GetDeviceCaps(dc, LOGPIXELSY)); + ReleaseDC(hwnd, dc); + if (dpi == 0) { + dpi = 96; + } + CreateFonts(); + + hProgress = CreateWindowExW(0, PROGRESS_CLASSW, nullptr, + WS_CHILD | PBS_SMOOTH, + 14, 139, 348, 5, + hwnd, reinterpret_cast(static_cast(100)), + hInst, nullptr); + if (hProgress) { + SendMessageW(hProgress, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); + SendMessageW(hProgress, PBM_SETPOS, 0, 0); + ShowWindow(hProgress, SW_HIDE); + } + + HICON icon = LoadIconW(hInst, MAKEINTRESOURCEW(1)); + if (icon) { + SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(icon)); + SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(icon)); + } + return true; +} + +void App::CreateFonts() +{ + auto create = [this](int points, int weight) -> HFONT { + return CreateFontW(-MulDiv(points, static_cast(dpi), 72), 0, 0, 0, weight, + FALSE, FALSE, FALSE, DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Tahoma"); + }; + fontTitle = create(15, FW_BOLD); + fontSub = create(9, FW_NORMAL); + fontMin = create(10, FW_NORMAL); + fontClose = create(9, FW_NORMAL); + fontText = create(9, FW_NORMAL); + fontBold = create(9, FW_BOLD); + fontGo = create(10, FW_BOLD); + fontArrow = create(8, FW_NORMAL); +} + +void App::DestroyFonts() +{ + for (HFONT* font : { &fontTitle, &fontSub, &fontMin, &fontClose, + &fontText, &fontBold, &fontGo, &fontArrow }) { + if (*font) { + DeleteObject(*font); + *font = nullptr; + } + } +} + +void App::ScanDisks() +{ + std::vector found = ScanUsbDisks(); + std::wstring newSig; + for (const auto& item : found) { + newSig += std::to_wstring(item.number) + L":" + std::to_wstring(item.size) + L"|"; + } + if (newSig == sig) { + return; + } + sig = newSig; + + int currentNumber = -1; + if (!disks.empty() && selIdx >= 0 && selIdx < static_cast(disks.size())) { + currentNumber = disks[selIdx].number; + } + + disks = std::move(found); + + if (disks.empty()) { + labelText = L"Подходящие накопители отсутствуют!"; + statusText = L"Найдено: 0"; + selIdx = 0; + installed = false; + ArrowState(); + RefreshGo(); + InvalidateAll(); + return; + } + + int foundIndex = 0; + for (int i = 0; i < static_cast(disks.size()); ++i) { + if (disks[i].number == currentNumber) { + foundIndex = i; + break; + } + } + selIdx = foundIndex; + ShowSelected(); + RefreshGo(); + labelText = L"Выбран накопитель:"; + statusText = Format(L"Найдено: %d", static_cast(disks.size())); + ArrowState(); + InvalidateAll(); +} + +void App::ShowSelected() +{ + if (disks.empty()) { + return; + } + if (selIdx < 0 || selIdx >= static_cast(disks.size())) { + selIdx = 0; + } + CheckInstalled(); + InvalidateAll(); +} + +void App::CheckInstalled() +{ + installed = false; + if (!disks.empty() && selIdx >= 0 && selIdx < static_cast(disks.size())) { + installed = DiskIsInstalled(disks[selIdx].number); + } + GoCaption(); +} + +void App::GoCaption() +{ + if (busy) { + return; + } + goText = installed ? L"Переустановить" : L"УСТАНОВИТЬ"; +} + +void App::GoState(bool on) +{ + goBg = on ? ColorFromHex(0x2563EB) : ColorFromHex(0xAEB6C4); + goFg = on ? RGB(255, 255, 255) : ColorFromHex(0xEDEFF3); +} + +void App::ArrowState() +{ + arrowFg = disks.empty() ? ColorFromHex(0xC7CBD3) : ColorFromHex(0x6B7280); +} + +void App::RefreshGo() +{ + canInstall = false; + if (!disks.empty() && selIdx >= 0 && selIdx < static_cast(disks.size())) { + canInstall = NominalGb(disks[selIdx].size) >= 32.0; + } + GoCaption(); + GoState(canInstall); +} + +void App::DimControls(bool on) +{ + dimmed = on; + if (on) { + dropBg = nameBg = sizeBg = arrowBg = ColorFromHex(0xF2F3F5); + nameFg = ColorFromHex(0xA0A6B1); + sizeFg = ColorFromHex(0xB4B9C2); + arrowFg = ColorFromHex(0xC7CBD3); + comboFrame = ColorFromHex(0xE3E6EB); + ClosePopup(); + } else { + dropBg = nameBg = sizeBg = arrowBg = RGB(255, 255, 255); + nameFg = ColorFromHex(0x1F2430); + sizeFg = ColorFromHex(0x6B7280); + ArrowState(); + comboFrame = ColorFromHex(0xD5DAE3); + } + InvalidateAll(); +} + +void App::BusySet(bool value) +{ + DebugLog(L"BusySet %d", value ? 1 : 0); + busy = value; + if (value) { + cancel = false; + phase = 0; + applied = 0; + DimControls(true); + goText = L"ОТМЕНИТЬ"; + goBg = ColorFromHex(0xC0392B); + goFg = RGB(255, 255, 255); + } else { + cancel = false; + phase = 0; + runJob = nullptr; + runProcess = nullptr; + runPid = 0; + CheckInstalled(); + DimControls(false); + RefreshGo(); + } + InvalidateAll(); +} + +void App::CancelLook(Hit hit, bool leftDown) +{ + COLORREF background; + COLORREF foreground; + if (cancel) { + background = ColorFromHex(0xAEB6C4); + foreground = ColorFromHex(0xEDEFF3); + } else { + background = ColorFromHex(0xC0392B); + foreground = RGB(255, 255, 255); + if (hit == Hit::Go) { + background = leftDown ? ColorFromHex(0xA93226) : ColorFromHex(0xD6453C); + } + } + goBg = background; + goFg = foreground; +} + +void App::SetWork(const std::wstring& text) +{ + workText = text; + InvalidateRect(hwnd, nullptr, FALSE); +} + +void App::ShowProgress(int value) +{ + if (!hProgress) { + return; + } + value = std::clamp(value, 0, 100); + if (!IsWindowVisible(hProgress)) { + ShowWindow(hProgress, SW_SHOW); + InvalidateRect(hwnd, nullptr, FALSE); + } + SendMessageW(hProgress, PBM_SETPOS, static_cast(value), 0); +} + +void App::HideProgress() +{ + if (hProgress && IsWindowVisible(hProgress)) { + ShowWindow(hProgress, SW_HIDE); + InvalidateRect(hwnd, nullptr, FALSE); + } +} + +void App::InvalidateAll() +{ + InvalidateRect(hwnd, nullptr, FALSE); + if (hPopup) { + InvalidateRect(hPopup, nullptr, FALSE); + } +} + +void App::InvalidatePopup() +{ + if (hPopup) { + InvalidateRect(hPopup, nullptr, FALSE); + } +} + +void App::Paint(HDC dc) +{ + RECT client{}; + GetClientRect(hwnd, &client); + int w = client.right; + int h = client.bottom; + if (w <= 0 || h <= 0) { + return; + } + + HDC memory = CreateCompatibleDC(dc); + HBITMAP bitmap = CreateCompatibleBitmap(dc, w, h); + HGDIOBJ oldBitmap = SelectObject(memory, bitmap); + + FillRectColor(memory, 0, 0, w, h, ColorFromHex(CLR_BG)); + FillRectColor(memory, 0, 0, 376, 42, ColorFromHex(CLR_HEAD)); + + DrawTextRect(memory, L"Paravis Flash", fontTitle, 16, 2, 300, 24, + RGB(255, 255, 255), DT_LEFT | DT_TOP); + DrawTextRect(memory, L"Запись загрузочной флешки", fontSub, 18, 26, 300, 14, + ColorFromHex(0xC7D0E0), DT_LEFT | DT_TOP); + + FillRectColor(memory, 306, 2, 30, 18, minBg); + DrawTextRect(memory, L"—", fontMin, 306, 2, 30, 18, + ColorFromHex(0xC7D0E0), DT_CENTER | DT_VCENTER); + FillRectColor(memory, 338, 2, 30, 18, closeBg); + DrawTextRect(memory, L"✕", fontClose, 338, 2, 30, 18, + ColorFromHex(0xC7D0E0), DT_CENTER | DT_VCENTER); + + FillRectColor(memory, 8, 45, 360, 46, RGB(255, 255, 255)); + DrawTextRect(memory, labelText, fontText, 16, 45, 340, 15, + ColorFromHex(0x6B7280), DT_LEFT | DT_TOP); + + FillRectColor(memory, 15, 61, 342, 28, comboFrame); + FillRectColor(memory, 16, 62, 340, 26, dropBg); + FillRectColor(memory, 21, 62, 240, 26, nameBg); + FillRectColor(memory, 262, 62, 74, 26, sizeBg); + FillRectColor(memory, 336, 62, 16, 26, arrowBg); + + std::wstring diskName; + std::wstring diskCap; + if (!disks.empty() && selIdx >= 0 && selIdx < static_cast(disks.size())) { + diskName = disks[selIdx].name; + diskCap = disks[selIdx].cap; + } + DrawTextRect(memory, diskName, fontText, 21, 62, 240, 26, nameFg, + DT_LEFT | DT_VCENTER); + DrawTextRect(memory, diskCap, fontText, 262, 62, 74, 26, sizeFg, + DT_RIGHT | DT_VCENTER); + DrawTextRect(memory, L"▼", fontArrow, 336, 62, 16, 26, arrowFg, + DT_CENTER | DT_VCENTER); + + DrawTextRect(memory, statusText, fontText, 14, 95, 348, 15, + ColorFromHex(0x6B7280), DT_LEFT | DT_TOP); + + if (busy) { + DrawTextRect(memory, workText, fontBold, 14, 115, 348, 18, + ColorFromHex(0x1F2430), DT_CENTER | DT_TOP); + } else { + DrawTextRect(memory, L"Все данные на выбранном диске будут уничтожены", + fontBold, 14, 115, 348, 18, ColorFromHex(0xC0392B), + DT_CENTER | DT_TOP); + } + + FillRectColor(memory, 14, 147, 348, 30, goBg); + DrawTextRect(memory, goText, fontGo, 14, 147, 348, 30, goFg, + DT_CENTER | DT_VCENTER); + + BitBlt(dc, 0, 0, w, h, memory, 0, 0, SRCCOPY); + SelectObject(memory, oldBitmap); + DeleteObject(bitmap); + DeleteDC(memory); +} + +void App::PaintPopup(HDC dc) +{ + RECT client{}; + GetClientRect(hwnd, &client); + int w = client.right; + int h = client.bottom; + if (w <= 0 || h <= 0) { + return; + } + + HDC memory = CreateCompatibleDC(dc); + HBITMAP bitmap = CreateCompatibleBitmap(dc, w, h); + HGDIOBJ oldBitmap = SelectObject(memory, bitmap); + + FillRectColor(memory, 0, 0, w, h, ColorFromHex(0xD5DAE3)); + + int count = static_cast(disks.size()); + for (int i = 0; i < count; ++i) { + int y = 1 + i * 24; + COLORREF background = (i == popupHover) ? ColorFromHex(0xEEF3FF) : RGB(255, 255, 255); + FillRectColor(memory, 1, y, 340, 24, background); + DrawTextRect(memory, L" " + disks[i].name, fontText, 1, y, 248, 24, + ColorFromHex(0x1F2430), DT_LEFT | DT_VCENTER); + DrawTextRect(memory, disks[i].cap + L" ", fontText, 249, y, 92, 24, + ColorFromHex(0x6B7280), DT_RIGHT | DT_VCENTER); + } + + BitBlt(dc, 0, 0, w, h, memory, 0, 0, SRCCOPY); + SelectObject(memory, oldBitmap); + DeleteObject(bitmap); + DeleteDC(memory); +} + +RECT App::PopupScreenRect() const +{ + RECT rect{}; + if (hPopup) { + GetWindowRect(hPopup, &rect); + } + return rect; +} + +void App::OpenPopup() +{ + if (hPopup || disks.empty() || busy || modal) { + return; + } + int count = static_cast(disks.size()); + RECT mainRect{}; + GetWindowRect(hwnd, &mainRect); + int popupWidth = 342; + int popupHeight = count * 24 + 2; + hPopup = CreateWindowExW(WS_EX_TOOLWINDOW, kPopupClass, L"", WS_POPUP, + mainRect.left + 15, mainRect.top + 89, + popupWidth, popupHeight, + hwnd, nullptr, hInst, this); + if (!hPopup) { + return; + } + popupHover = -1; + popupMouseDown = false; + dropGuard = GetTickCount64(); + ShowWindow(hPopup, SW_SHOWNOACTIVATE); + UpdateWindow(hPopup); +} + +void App::ClosePopup() +{ + if (hPopup) { + HWND popup = hPopup; + hPopup = nullptr; + DestroyWindow(popup); + } + popupHover = -1; +} + +void App::TogglePopup() +{ + if (hPopup) { + ClosePopup(); + } else { + OpenPopup(); + } +} + +void App::HandlePopup(POINT screen, bool leftDown) +{ + if (!hPopup) { + return; + } + + POINT point = screen; + ScreenToClient(hPopup, &point); + RECT client{}; + GetClientRect(hPopup, &client); + bool inside = PtInRect(&client, point) != FALSE; + + int hover = -1; + int count = static_cast(disks.size()); + if (inside && point.x >= 1 && point.x < 341 && point.y >= 1) { + int index = (point.y - 1) / 24; + if (index >= 0 && index < count && point.y < 1 + count * 24) { + hover = index; + } + } + + if (hover != popupHover) { + popupHover = hover; + InvalidatePopup(); + } + + if (leftDown && GetTickCount64() - dropGuard > 400) { + if (hover >= 0) { + selIdx = hover; + ShowSelected(); + RefreshGo(); + ClosePopup(); + } else if (!inside) { + ClosePopup(); + } + } +} + +bool App::GoPoll(Hit hit, bool leftDown) +{ + if (hit == Hit::Go && leftDown) { + goPollDown = true; + return false; + } + if (goPollDown) { + goPollDown = false; + if (hit == Hit::Go && !leftDown) { + if (GetTickCount64() - clickGuard < 400) { + return false; + } + return true; + } + } + return false; +} + +void App::OnPoll() +{ + if (modal) { + return; + } + + POINT screen{}; + GetCursorPos(&screen); + bool leftDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + + bool overPopup = false; + if (hPopup) { + RECT popupRect = PopupScreenRect(); + overPopup = PtInRect(&popupRect, screen) != FALSE; + } + + POINT client = screen; + ScreenToClient(hwnd, &client); + Hit hit = overPopup ? Hit::None : HitTest(client); + + if (busy) { + if (!cancel) { + CancelLook(hit, leftDown); + if (GoPoll(hit, leftDown)) { + ConfirmCancel(); + } + } else { + CancelLook(hit, leftDown); + } + if (hPopup) { + ClosePopup(); + } + InvalidateAll(); + return; + } + + if (GoPoll(hit, leftDown)) { + DebugLog(L"GO click -> Apply"); + Apply(); + InvalidateAll(); + return; + } + + COLORREF minColor = ColorFromHex(CLR_HEAD); + COLORREF closeColor = ColorFromHex(CLR_HEAD); + if (hit == Hit::Min) { + minColor = leftDown ? ColorFromHex(0x3A4A6B) : ColorFromHex(0x2C3A57); + } + if (hit == Hit::Close) { + closeColor = leftDown ? ColorFromHex(0xC0392B) : ColorFromHex(0xE04B3F); + } + minBg = minColor; + closeBg = closeColor; + + if (hit == Hit::Min && leftDown) { + minDown = true; + } else if (minDown) { + minDown = false; + if (hit == Hit::Min && !leftDown) { + ShowWindow(hwnd, SW_MINIMIZE); + } + } + if (hit == Hit::Close && leftDown) { + closeDown = true; + } else if (closeDown) { + closeDown = false; + if (hit == Hit::Close && !leftDown) { + DebugLog(L"close button click"); + DestroyWindow(hwnd); + return; + } + } + + if ((hit == Hit::Header || hit == Hit::Title || hit == Hit::Sub) && leftDown && !dragging) { + POINT mouse{}; + RECT windowRect{}; + GetCursorPos(&mouse); + GetWindowRect(hwnd, &windowRect); + dragging = true; + dragX = mouse.x - windowRect.left; + dragY = mouse.y - windowRect.top; + } + if (dragging) { + if (!leftDown) { + dragging = false; + } else { + POINT mouse{}; + GetCursorPos(&mouse); + SetWindowPos(hwnd, nullptr, mouse.x - dragX, mouse.y - dragY, + 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + } + } + + COLORREF background; + if (!canInstall) { + background = ColorFromHex(0xAEB6C4); + } else if (hit == Hit::Go && leftDown) { + background = ColorFromHex(0x1B4FD8); + } else if (hit == Hit::Go) { + background = ColorFromHex(0x3B82F6); + } else { + background = ColorFromHex(0x2563EB); + } + goBg = background; + goFg = canInstall ? RGB(255, 255, 255) : ColorFromHex(0xEDEFF3); + + bool comboHover = !disks.empty() && + (hit == Hit::Combo || hit == Hit::Drop || hit == Hit::DropName || + hit == Hit::DropSize || hit == Hit::Arrow); + comboFrame = comboHover ? ColorFromHex(0x7FA6F0) : ColorFromHex(0xD5DAE3); + + if (!disks.empty() && + (hit == Hit::Drop || hit == Hit::Arrow || hit == Hit::DropName || + hit == Hit::DropSize) && leftDown && !dropDownGui) { + TogglePopup(); + dropDownGui = true; + } + if (!leftDown) { + dropDownGui = false; + } + + if (hPopup) { + HandlePopup(screen, leftDown); + } + + InvalidateAll(); +} + +void App::OnAutoRefresh() +{ + if (!needScan) { + return; + } + if (busy || modal || hPopup) { + return; + } + if (GetTickCount64() - devTick < 500) { + return; + } + needScan = false; + ScanDisks(); +} + +void App::OnDeviceChange(WPARAM wParam) +{ + if (wParam == 0x8000 || wParam == 0x8004 || wParam == 0x0007) { + if (!needScan) { + needScan = true; + devTick = GetTickCount64(); + } + } +} + +void App::PumpMessages() +{ + MSG message{}; + while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { + if (message.message == WM_QUIT) { + PostQuitMessage(static_cast(message.wParam)); + continue; + } + TranslateMessage(&message); + DispatchMessageW(&message); + } +} + +void App::SleepPump(int milliseconds) +{ + ULONGLONG end = GetTickCount64() + static_cast(milliseconds); + while (GetTickCount64() < end) { + ULONGLONG remaining = end - GetTickCount64(); + DWORD wait = static_cast(std::min(remaining, 50)); + MsgWaitForMultipleObjects(0, nullptr, FALSE, wait, QS_ALLINPUT); + PumpMessages(); + } +} + +bool App::WaitForProcess(HANDLE process) +{ + for (;;) { + DWORD result = MsgWaitForMultipleObjects(1, &process, FALSE, 30, QS_ALLINPUT); + if (result == WAIT_OBJECT_0) { + return true; + } + // Always pump: PeekMessage is what materializes due WM_TIMER messages, + // so this keeps the cancel button responsive during diskpart. + PumpMessages(); + } +} + +Hit App::HitTest(POINT pt) const +{ + auto inside = [&pt](int x, int y, int w, int h) { + return pt.x >= x && pt.y >= y && pt.x < x + w && pt.y < y + h; + }; + + if (inside(306, 2, 30, 18)) { + return Hit::Min; + } + if (inside(338, 2, 30, 18)) { + return Hit::Close; + } + if (inside(336, 62, 16, 26)) { + return Hit::Arrow; + } + if (inside(21, 62, 240, 26)) { + return Hit::DropName; + } + if (inside(262, 62, 74, 26)) { + return Hit::DropSize; + } + if (inside(16, 62, 340, 26)) { + return Hit::Drop; + } + if (inside(15, 61, 342, 28)) { + return Hit::Combo; + } + if (inside(14, 147, 348, 30)) { + return Hit::Go; + } + if (inside(16, 2, 300, 24)) { + return Hit::Title; + } + if (inside(18, 26, 300, 14)) { + return Hit::Sub; + } + if (inside(0, 0, 376, 42)) { + return Hit::Header; + } + if (inside(8, 45, 360, 46)) { + return Hit::Card; + } + return Hit::None; +} + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) +{ + int argc = 0; + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); + std::vector arguments; + if (argv) { + for (int i = 1; i < argc; ++i) { + arguments.emplace_back(argv[i]); + } + LocalFree(argv); + } + + DebugLog(L"=== start pid=%lu args=%zu", GetCurrentProcessId(), arguments.size()); + App app; + app.hInst = hInstance; + app.runStamp = GetRunStamp(); + + for (const std::wstring& argument : arguments) { + std::wstring lower = argument; + for (wchar_t& ch : lower) { + ch = static_cast(towlower(ch)); + } + + if (lower == L"/selftest") { + WriteSelftest(); + return 0; + } + if (lower.rfind(L"/verify=", 0) == 0) { + WriteVerify(_wtoi(argument.c_str() + 8)); + return 0; + } + if (lower.rfind(L"/testdisk=", 0) == 0) { + app.testDisk = _wtoi(argument.c_str() + 10); + } + if (lower.rfind(L"/wim=", 0) == 0) { + app.wimPath = argument.substr(5); + } + } + + INITCOMMONCONTROLSEX commonControls{}; + commonControls.dwSize = sizeof(commonControls); + commonControls.dwICC = ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES; + InitCommonControlsEx(&commonControls); + + WNDCLASSEXW windowClass{}; + windowClass.cbSize = sizeof(windowClass); + windowClass.lpfnWndProc = MainProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.hIcon = LoadIconW(hInstance, MAKEINTRESOURCEW(1)); + windowClass.hIconSm = LoadIconW(hInstance, MAKEINTRESOURCEW(1)); + windowClass.lpszClassName = kMainClass; + if (!RegisterClassExW(&windowClass)) { + return 1; + } + + WNDCLASSEXW popupClass = windowClass; + popupClass.lpfnWndProc = PopupProc; + popupClass.lpszClassName = kPopupClass; + popupClass.hIcon = nullptr; + popupClass.hIconSm = nullptr; + if (!RegisterClassExW(&popupClass)) { + return 1; + } + + RECT workArea{}; + SystemParametersInfoW(SPI_GETWORKAREA, 0, &workArea, 0); + int workWidth = workArea.right - workArea.left; + int workHeight = workArea.bottom - workArea.top; + int x = workArea.left + (workWidth - app.width) / 2; + int y = workArea.top + (workHeight - app.height) / 2; + + app.hwnd = CreateWindowExW(WS_EX_APPWINDOW, kMainClass, L"Paravis installer", + WS_POPUP | WS_CLIPCHILDREN, + x, y, app.width, app.height, + nullptr, nullptr, hInstance, &app); + if (!app.hwnd) { + return 1; + } + + ShowWindow(app.hwnd, SW_SHOW); + UpdateWindow(app.hwnd); + SetForegroundWindow(app.hwnd); + + SetTimer(app.hwnd, 1, 40, nullptr); + SetTimer(app.hwnd, 2, 250, nullptr); + + app.ScanDisks(); + + MSG message{}; + while (GetMessageW(&message, nullptr, 0, 0) > 0) { + TranslateMessage(&message); + DispatchMessageW(&message); + } + DebugLog(L"message loop ended wParam=%llu", static_cast(message.wParam)); + return static_cast(message.wParam); +} + + diff --git a/src/sec.ico b/src/sec.ico new file mode 100644 index 0000000..401ca08 Binary files /dev/null and b/src/sec.ico differ diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..d74ccf4 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,226 @@ +#include "util.h" + +#include +#include +#include + +std::wstring Format(const wchar_t* fmt, ...) +{ + va_list args; + va_start(args, fmt); + va_list copy; + va_copy(copy, args); + int needed = _vscwprintf(fmt, copy); + va_end(copy); + + if (needed < 0) { + va_end(args); + return std::wstring(); + } + + std::wstring result(static_cast(needed), L'\0'); + if (needed > 0) { + _vsnwprintf_s(result.data(), result.size() + 1, _TRUNCATE, fmt, args); + } + va_end(args); + return result; +} + +std::wstring GetTempDir() +{ + wchar_t buffer[MAX_PATH + 1] = {}; + DWORD length = GetTempPathW(MAX_PATH, buffer); + if (length == 0 || length >= MAX_PATH) { + wchar_t windows[MAX_PATH] = {}; + GetWindowsDirectoryW(windows, MAX_PATH); + return JoinPath(windows, L"Temp") + L"\\"; + } + std::wstring path(buffer, length); + if (path.empty() || path.back() != L'\\') { + path.push_back(L'\\'); + } + return path; +} + +std::wstring GetExeDir() +{ + wchar_t buffer[MAX_PATH * 4] = {}; + DWORD length = GetModuleFileNameW(nullptr, buffer, static_cast(std::size(buffer))); + if (length == 0) { + return std::wstring(); + } + std::wstring path(buffer, length); + size_t slash = path.find_last_of(L"\\/"); + if (slash != std::wstring::npos) { + path.resize(slash); + } + return path; +} + +std::wstring GetRunStamp() +{ + SYSTEMTIME st{}; + GetLocalTime(&st); + return Format(L"%04d%02d%02d_%02d%02d%02d", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); +} + +std::wstring JoinPath(const std::wstring& dir, const std::wstring& name) +{ + if (dir.empty()) { + return name; + } + if (dir.back() == L'\\' || dir.back() == L'/') { + return dir + name; + } + return dir + L"\\" + name; +} + +bool FileExists(const std::wstring& path) +{ + DWORD attributes = GetFileAttributesW(path.c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +bool IsAdmin() +{ + HANDLE token = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + return false; + } + + TOKEN_ELEVATION elevation{}; + DWORD returned = 0; + bool elevated = false; + if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &returned)) { + elevated = elevation.TokenIsElevated != 0; + } + CloseHandle(token); + return elevated; +} + +std::string WideToAnsi(const std::wstring& text, UINT codepage) +{ + if (text.empty()) { + return std::string(); + } + int needed = WideCharToMultiByte(codepage, 0, text.c_str(), static_cast(text.size()), + nullptr, 0, nullptr, nullptr); + if (needed <= 0) { + return std::string(); + } + std::string result(static_cast(needed), '\0'); + WideCharToMultiByte(codepage, 0, text.c_str(), static_cast(text.size()), + result.data(), needed, nullptr, nullptr); + return result; +} + +std::wstring AnsiToWide(const std::string& text, UINT codepage) +{ + if (text.empty()) { + return std::wstring(); + } + int needed = MultiByteToWideChar(codepage, 0, text.data(), static_cast(text.size()), + nullptr, 0); + if (needed <= 0) { + return std::wstring(); + } + std::wstring result(static_cast(needed), L'\0'); + MultiByteToWideChar(codepage, 0, text.data(), static_cast(text.size()), + result.data(), needed); + return result; +} + +bool WriteAnsiFile(const std::wstring& path, const std::wstring& text) +{ + std::string bytes = WideToAnsi(text, CP_ACP); + HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) { + return false; + } + + DWORD written = 0; + bool ok = true; + if (!bytes.empty()) { + ok = WriteFile(file, bytes.data(), static_cast(bytes.size()), &written, nullptr) != FALSE + && written == bytes.size(); + } + CloseHandle(file); + return ok; +} + +std::string ReadFileBytes(const std::wstring& path) +{ + HANDLE file = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) { + return std::string(); + } + + LARGE_INTEGER size{}; + if (!GetFileSizeEx(file, &size) || size.QuadPart <= 0) { + CloseHandle(file); + return std::string(); + } + if (size.QuadPart > 32 * 1024 * 1024) { + size.QuadPart = 32 * 1024 * 1024; + } + + std::string data(static_cast(size.QuadPart), '\0'); + DWORD read = 0; + bool ok = ReadFile(file, data.data(), static_cast(data.size()), &read, nullptr) != FALSE; + CloseHandle(file); + if (!ok) { + return std::string(); + } + data.resize(read); + return data; +} + +std::wstring GetComSpec() +{ + wchar_t buffer[MAX_PATH * 2] = {}; + DWORD length = GetEnvironmentVariableW(L"ComSpec", buffer, static_cast(std::size(buffer))); + if (length > 0 && length < std::size(buffer)) { + return std::wstring(buffer, length); + } + return L"C:\\Windows\\System32\\cmd.exe"; +} + +COLORREF ColorFromHex(DWORD rgb) +{ + return RGB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF); +} + +void DebugLog(const wchar_t* fmt, ...) +{ + wchar_t enabled[8] = {}; + if (GetEnvironmentVariableW(L"PARAVIS_DEBUG_LOG", enabled, + static_cast(std::size(enabled))) == 0) { + return; + } + + va_list args; + va_start(args, fmt); + wchar_t message[4096] = {}; + _vsnwprintf_s(message, std::size(message), _TRUNCATE, fmt, args); + va_end(args); + + SYSTEMTIME st{}; + GetLocalTime(&st); + std::wstring line = Format(L"%02d:%02d:%02d.%03d [%lu] %s\r\n", + st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, + GetCurrentThreadId(), message); + + std::string utf8 = WideToAnsi(line, CP_UTF8); + std::wstring path = GetTempDir() + L"paravis_cpp_debug.log"; + HANDLE file = CreateFileW(path.c_str(), FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file != INVALID_HANDLE_VALUE) { + DWORD written = 0; + WriteFile(file, utf8.data(), static_cast(utf8.size()), &written, nullptr); + CloseHandle(file); + } +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..b930f1f --- /dev/null +++ b/src/util.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +std::wstring Format(const wchar_t* fmt, ...); +std::wstring GetTempDir(); +std::wstring GetExeDir(); +std::wstring GetRunStamp(); +std::wstring JoinPath(const std::wstring& dir, const std::wstring& name); +bool FileExists(const std::wstring& path); +bool IsAdmin(); +std::string WideToAnsi(const std::wstring& text, UINT codepage = CP_ACP); +std::wstring AnsiToWide(const std::string& text, UINT codepage = CP_ACP); +bool WriteAnsiFile(const std::wstring& path, const std::wstring& text); +std::string ReadFileBytes(const std::wstring& path); +std::wstring GetComSpec(); +COLORREF ColorFromHex(DWORD rgb); +void DebugLog(const wchar_t* fmt, ...);