xetup/internal/preflight/preflight.go
X9 Dev 64646f1b7f feat: email report, pre-flight checks, parallel winget installs
Email report: HTML summary sent via SMTP2Go (mail-eu.smtp2go.com)
at the end of every deployment. Subject "xetup report HOSTNAME",
body contains per-step status table with timestamps. Non-blocking
(goroutine) so it doesn't delay the summary screen.

Pre-flight checks: admin rights, winget availability, network
connectivity (DNS resolve), and disk space verified before the
config form. Results shown as colored status lines at the top
of the GUI - red warnings tell the technician what's wrong
before starting a 30-minute deployment.

Parallel winget: 02-software.ps1 now launches all winget installs
as background jobs (Start-Job) and waits for all to complete.
7-Zip, Acrobat, OpenVPN run simultaneously instead of sequentially,
saving 3-5 minutes per deployment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 12:26:22 +02:00

111 lines
2.7 KiB
Go

//go:build windows
// Package preflight runs quick environment checks before deployment starts.
// Each check returns a human-readable result; failures are warnings, not blockers.
package preflight
import (
"fmt"
"os/exec"
"strings"
"syscall"
"unsafe"
)
// Result is one pre-flight check outcome.
type Result struct {
Name string
OK bool
Detail string
}
// RunAll executes all pre-flight checks and returns results.
func RunAll() []Result {
return []Result{
checkAdmin(),
checkWinget(),
checkNetwork(),
checkDisk(),
}
}
func checkAdmin() Result {
r := Result{Name: "Spusteno jako Administrator"}
// If we got here via #Requires -RunAsAdministrator or manifested exe,
// we're admin. Double-check via net session.
cmd := exec.Command("net", "session")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
if err := cmd.Run(); err != nil {
r.OK = false
r.Detail = "Neni spusteno jako Administrator"
return r
}
r.OK = true
r.Detail = "OK"
return r
}
func checkWinget() Result {
r := Result{Name: "Winget dostupny"}
cmd := exec.Command("winget", "--version")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, err := cmd.CombinedOutput()
if err != nil {
r.OK = false
r.Detail = "winget nenalezen - software se nenainstaluje"
return r
}
ver := strings.TrimSpace(string(out))
r.OK = true
r.Detail = ver
return r
}
func checkNetwork() Result {
r := Result{Name: "Pripojeni k internetu"}
// Try to resolve a well-known hostname
cmd := exec.Command("powershell.exe", "-NonInteractive", "-Command",
"[System.Net.Dns]::GetHostEntry('www.google.com').AddressList[0].IPAddressToString")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, err := cmd.CombinedOutput()
if err != nil || strings.TrimSpace(string(out)) == "" {
r.OK = false
r.Detail = "DNS resolve selhal - Atera a winget nebudou fungovat"
return r
}
r.OK = true
r.Detail = "OK"
return r
}
func checkDisk() Result {
r := Result{Name: "Volne misto na C:"}
kernel32 := syscall.NewLazyDLL("kernel32.dll")
getDiskFreeSpaceEx := kernel32.NewProc("GetDiskFreeSpaceExW")
var freeBytesAvailable, totalBytes, totalFreeBytes uint64
drive, _ := syscall.UTF16PtrFromString("C:\\")
ret, _, _ := getDiskFreeSpaceEx.Call(
uintptr(unsafe.Pointer(drive)),
uintptr(unsafe.Pointer(&freeBytesAvailable)),
uintptr(unsafe.Pointer(&totalBytes)),
uintptr(unsafe.Pointer(&totalFreeBytes)),
)
if ret == 0 {
r.OK = false
r.Detail = "Nelze zjistit volne misto"
return r
}
freeGB := float64(freeBytesAvailable) / (1024 * 1024 * 1024)
if freeGB < 5 {
r.OK = false
r.Detail = fmt.Sprintf("%.1f GB - malo mista (min 5 GB)", freeGB)
return r
}
r.OK = true
r.Detail = fmt.Sprintf("%.1f GB volnych", freeGB)
return r
}