Fixes, Windows Update (step 12), auto-reboot, PS window hide

- Write-Log creates C:\Windows\Setup\Scripts\ automatically (was failing on fresh install)
- Step 12: PSWindowsUpdate first pass + X9-WindowsUpdate scheduled task for post-reboot rounds
  (handles typical 2-3 reboot cycles on fresh Windows, task self-deletes when up to date)
- GUI summary: 60s countdown auto-reboot with "Restartovat ted" / "Zrusit restart" buttons
- runner: HideWindow=true prevents PS console from appearing over GUI
- runner: skipPSNoiseLine filters PS error metadata (CategoryInfo, FullyQualifiedErrorId etc.)
- web: fix curl command to include https:// prefix
This commit is contained in:
Filip Zubik 2026-04-16 14:49:41 +02:00
parent e62cbaaec3
commit 7e6095d1bd
20 changed files with 247 additions and 5 deletions

View file

@ -77,6 +77,7 @@ func DefaultConfig() Config {
"backinfo": true, "backinfo": true,
"activation": true, "activation": true,
"dellUpdate": true, "dellUpdate": true,
"windowsUpdate": true,
"network": true, "network": true,
"pcIdentity": true, "pcIdentity": true,
}, },

View file

@ -14,6 +14,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"os/exec"
"sync" "sync"
"time" "time"
@ -324,11 +325,16 @@ func runPhase(runCfg runner.RunConfig, steps []runner.Step) []runner.Result {
} }
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// Phase 3 Summary // Phase 3 Summary with auto-reboot countdown
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
const rebootCountdown = 60 // seconds before automatic reboot
func donePhase(results []runner.Result) { func donePhase(results []runner.Result) {
var mw *walk.MainWindow var (
mw *walk.MainWindow
countdownLbl *walk.Label
)
ok, errs, skipped := 0, 0, 0 ok, errs, skipped := 0, 0, 0
rows := make([]Widget, 0, len(results)) rows := make([]Widget, 0, len(results))
@ -358,6 +364,9 @@ func donePhase(results []runner.Result) {
summaryText := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped) summaryText := fmt.Sprintf("OK: %d CHYBY: %d PRESKOCENO: %d", ok, errs, skipped)
// cancelled by "Zrusit restart" button
cancelReboot := make(chan struct{})
if err := (MainWindow{ if err := (MainWindow{
AssignTo: &mw, AssignTo: &mw,
Title: "xetup \u2014 hotovo", Title: "xetup \u2014 hotovo",
@ -371,7 +380,7 @@ func donePhase(results []runner.Result) {
}, },
HSeparator{}, HSeparator{},
ScrollView{ ScrollView{
MinSize: Size{Height: 560}, MinSize: Size{Height: 510},
Layout: VBox{MarginsZero: true}, Layout: VBox{MarginsZero: true},
Children: rows, Children: rows,
}, },
@ -381,16 +390,34 @@ func donePhase(results []runner.Result) {
Alignment: AlignHCenterVNear, Alignment: AlignHCenterVNear,
Font: Font{Bold: true}, Font: Font{Bold: true},
}, },
Label{
AssignTo: &countdownLbl,
Text: fmt.Sprintf("Restart za %ds...", rebootCountdown),
Alignment: AlignHCenterVNear,
},
Composite{ Composite{
Layout: HBox{MarginsZero: true}, Layout: HBox{MarginsZero: true},
Children: []Widget{ Children: []Widget{
HSpacer{}, HSpacer{},
PushButton{ PushButton{
Text: " ZAVRIT ", Text: " Restartovat ted ",
OnClicked: func() { OnClicked: func() {
close(cancelReboot) // signal goroutine to stop ticker
reboot()
mw.Close() mw.Close()
}, },
}, },
PushButton{
Text: " Zrusit restart ",
OnClicked: func() {
select {
case <-cancelReboot: // already closed
default:
close(cancelReboot)
}
countdownLbl.SetText("Restart zrusen.")
},
},
HSpacer{}, HSpacer{},
}, },
}, },
@ -399,9 +426,42 @@ func donePhase(results []runner.Result) {
return return
} }
// Countdown goroutine
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
remaining := rebootCountdown
for {
select {
case <-cancelReboot:
return
case <-ticker.C:
remaining--
r := remaining
mw.Synchronize(func() {
if r > 0 {
countdownLbl.SetText(fmt.Sprintf("Restart za %ds...", r))
} else {
countdownLbl.SetText("Restartuji...")
}
})
if remaining <= 0 {
reboot()
mw.Synchronize(func() { mw.Close() })
return
}
}
}
}()
mw.Run() mw.Run()
} }
// reboot issues an immediate Windows restart.
func reboot() {
exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck
}
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// Helpers // Helpers
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------

View file

@ -0,0 +1,7 @@
//go:build !windows
package runner
import "os/exec"
func hideWindow(cmd *exec.Cmd) {}

View file

@ -0,0 +1,14 @@
//go:build windows
package runner
import (
"os/exec"
"syscall"
)
// hideWindow prevents the child process from creating a visible console window.
// Without this, powershell.exe opens a full-size window on top of the GUI.
func hideWindow(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
}

View file

@ -36,6 +36,7 @@ func AllSteps() []Step {
{ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.ps1"}, {ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.ps1"},
{ID: "activation", Num: "08", Name: "Windows aktivace", ScriptName: "08-activation.ps1"}, {ID: "activation", Num: "08", Name: "Windows aktivace", ScriptName: "08-activation.ps1"},
{ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"}, {ID: "dellUpdate", Num: "11", Name: "Dell Command | Update", ScriptName: "11-dell-update.ps1"},
{ID: "windowsUpdate", Num: "12", Name: "Windows Update", ScriptName: "12-windows-update.ps1"},
{ID: "network", Num: "09", Name: "Network discovery", ScriptName: "10-network.ps1"}, {ID: "network", Num: "09", Name: "Network discovery", ScriptName: "10-network.ps1"},
{ID: "pcIdentity", Num: "10", Name: "PC identita", ScriptName: "09-pc-identity.ps1"}, {ID: "pcIdentity", Num: "10", Name: "PC identita", ScriptName: "09-pc-identity.ps1"},
} }
@ -222,6 +223,7 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error
} }
cmd := exec.CommandContext(ctx, "powershell.exe", args...) cmd := exec.CommandContext(ctx, "powershell.exe", args...)
hideWindow(cmd) // prevent PS console window from appearing over the GUI
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
@ -236,6 +238,9 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error
scanner := bufio.NewScanner(stdout) scanner := bufio.NewScanner(stdout)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if skipPSNoiseLine(line) {
continue
}
r.onLog(LogLine{ r.onLog(LogLine{
StepID: step.ID, StepID: step.ID,
Text: line, Text: line,
@ -246,6 +251,33 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error
return cmd.Wait() return cmd.Wait()
} }
// skipPSNoiseLine returns true for PowerShell stderr noise that clutters the log:
// multi-line error blocks (At line:N, CategoryInfo, FullyQualifiedErrorId, etc.),
// blank lines, and VERBOSE: prefix lines already handled by Write-Log.
func skipPSNoiseLine(line string) bool {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
return true
}
for _, prefix := range []string{
"At line:",
"+ CategoryInfo",
"+ FullyQualifiedErrorId",
"+ PositionMessage",
"VERBOSE:",
"DEBUG:",
} {
if strings.HasPrefix(trimmed, prefix) {
return true
}
}
// PS error continuation lines start with spaces + "+" or "~"
if len(trimmed) > 0 && (trimmed[0] == '+' || strings.HasPrefix(trimmed, "~")) {
return true
}
return false
}
// parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message". // parseLevel extracts the log level from lines formatted as "[HH:mm:ss] [LEVEL] message".
func parseLevel(line string) string { func parseLevel(line string) string {
if strings.Contains(line, "] [OK]") { if strings.Contains(line, "] [OK]") {

View file

@ -27,6 +27,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -23,6 +23,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -28,6 +28,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -37,6 +37,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -36,6 +36,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -23,6 +23,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -25,6 +25,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -25,6 +25,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -24,6 +24,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
} }

View file

@ -26,6 +26,7 @@ $ErrorActionPreference = "Continue"
function Write-Log { function Write-Log {
param([string]$Message, [string]$Level = "INFO") param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message" $line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8 Add-Content -Path $LogFile -Value $line -Encoding UTF8
switch ($Level) { switch ($Level) {
"OK" { Write-Host $line -ForegroundColor Green } "OK" { Write-Host $line -ForegroundColor Green }

View file

@ -0,0 +1,115 @@
<#
.SYNOPSIS
Installs all available Windows Updates via PSWindowsUpdate module.
.DESCRIPTION
First pass: installs all currently available updates without rebooting.
Then registers a scheduled task "X9-WindowsUpdate" that runs on every
logon (as SYSTEM) until no more updates are found - handles the typical
2-3 reboot cycles required on a fresh Windows installation.
Operator workflow:
1. xetup completes all steps
2. Operator reboots manually
3. On each subsequent logon the scheduled task runs another update pass
4. Task removes itself automatically when system is fully up to date
.ITEMS
nainstalovat-pswindowsupdate-modul: Installs NuGet provider and PSWindowsUpdate module from PSGallery.
spustit-prvni-kolo-windows-update: First update pass without reboot - installs all currently available updates.
registrovat-scheduled-task-pro-dalsi-kola: Registers X9-WindowsUpdate scheduled task that runs on logon, handles post-reboot update rounds, and self-deletes when no more updates are found.
#>
param(
[object]$Config,
[string]$LogFile
)
$ErrorActionPreference = "Continue"
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'HH:mm:ss')] [$Level] $Message"
$null = New-Item -ItemType Directory -Force -Path (Split-Path $LogFile -Parent) -ErrorAction SilentlyContinue
Add-Content -Path $LogFile -Value $line -Encoding UTF8
Write-Output $line
}
Write-Log "=== Step 12 - Windows Update ===" -Level STEP
# -----------------------------------------------------------------------
# 1. NuGet provider + PSWindowsUpdate module
# -----------------------------------------------------------------------
Write-Log "Setting up PSWindowsUpdate module..." -Level INFO
try {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope AllUsers | Out-Null
$existing = Get-Module -ListAvailable -Name PSWindowsUpdate | Select-Object -First 1
if ($existing) {
Write-Log " PSWindowsUpdate $($existing.Version) already installed" -Level INFO
} else {
Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -AllowClobber | Out-Null
Write-Log " PSWindowsUpdate installed" -Level OK
}
Import-Module PSWindowsUpdate -Force
} catch {
Write-Log " Module setup failed: $_" -Level ERROR
exit 1
}
# -----------------------------------------------------------------------
# 2. First update pass (no reboot)
# -----------------------------------------------------------------------
Write-Log "Running first Windows Update pass..." -Level INFO
try {
$result = Install-WindowsUpdate -AcceptAll -IgnoreReboot -Verbose 2>&1
$installed = @($result | Where-Object { $_ -match 'KB\d+|Downloaded|Installed' })
if ($installed.Count -gt 0) {
$result | Where-Object { "$_" -match '\S' } | ForEach-Object { Write-Log " $_" -Level INFO }
Write-Log " First pass complete - reboot required for remaining rounds" -Level OK
} else {
Write-Log " System already up to date" -Level OK
}
} catch {
Write-Log " First pass failed: $_" -Level ERROR
}
# -----------------------------------------------------------------------
# 3. Scheduled task for post-reboot update rounds (self-deleting)
# -----------------------------------------------------------------------
Write-Log "Registering post-reboot update task..." -Level INFO
$taskName = "X9-WindowsUpdate"
# PowerShell block that runs on each logon until no more updates found
$updateScript = @'
Import-Module PSWindowsUpdate -Force -ErrorAction Stop
$updates = Get-WindowsUpdate -AcceptAll -IgnoreReboot
if ($updates) {
Install-WindowsUpdate -AcceptAll -IgnoreReboot | Out-File "C:\Windows\Setup\Scripts\wu-pass-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8
} else {
# No more updates - remove this task
Unregister-ScheduledTask -TaskName "X9-WindowsUpdate" -Confirm:$false
}
'@
try {
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"$updateScript`""
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
-MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
# Remove existing task first (idempotent)
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger `
-Settings $settings -Principal $principal -Force | Out-Null
Write-Log " Task '$taskName' registered - runs on each logon until fully updated" -Level OK
} catch {
Write-Log " Failed to register scheduled task: $_" -Level WARN
Write-Log " Manual Windows Update rounds will be needed after reboot" -Level WARN
}
Write-Log "Step 12 - Windows Update complete" -Level OK
Write-Log " ACTION REQUIRED: Reboot the machine to complete remaining update rounds" -Level WARN

View file

@ -178,7 +178,7 @@
</div> </div>
<div class="install-box"> <div class="install-box">
<code id="install-cmd">curl -Lo xetup.exe xetup.x9.cz/dl</code> <code id="install-cmd">curl -Lo xetup.exe https://xetup.x9.cz/dl</code>
<button id="copy-btn" onclick="copyCmd()">Kopirovat</button> <button id="copy-btn" onclick="copyCmd()">Kopirovat</button>
</div> </div>