From ce8b49d0ec960ef57d83f2054eee42cfd8cdcbe4 Mon Sep 17 00:00:00 2001 From: Paravis Date: Sat, 7 Nov 2026 20:40:47 +0300 Subject: [PATCH] Paravis USB installer 2.0 (C++ port) --- .gitignore | 40 ++ CMakeLists.txt | 49 ++ LICENSE | 21 + README.md | 74 +++ build.cmd | 9 + legacy-au3/README-legacy.md | 64 ++ legacy-au3/theme.manifest | 15 + legacy-au3/usb-install.au3 | 1191 +++++++++++++++++++++++++++++++++++ src/app.h | 151 +++++ src/app.manifest | 22 + src/app.rc | 32 + src/disks.cpp | 496 +++++++++++++++ src/disks.h | 28 + src/install.cpp | 694 ++++++++++++++++++++ src/main.cpp | 987 +++++++++++++++++++++++++++++ src/sec.ico | Bin 0 -> 31826 bytes src/util.cpp | 226 +++++++ src/util.h | 20 + 18 files changed, 4119 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.cmd create mode 100644 legacy-au3/README-legacy.md create mode 100644 legacy-au3/theme.manifest create mode 100644 legacy-au3/usb-install.au3 create mode 100644 src/app.h create mode 100644 src/app.manifest create mode 100644 src/app.rc create mode 100644 src/disks.cpp create mode 100644 src/disks.h create mode 100644 src/install.cpp create mode 100644 src/main.cpp create mode 100644 src/sec.ico create mode 100644 src/util.cpp create mode 100644 src/util.h 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 0000000000000000000000000000000000000000..401ca087058da559fb472a528033eebcb95271ec GIT binary patch literal 31826 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 ULB3xH4+X*#zUrO1YQTX10g;9lj{pDw literal 0 HcmV?d00001 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, ...);