diff --git a/internal/config/config.go b/internal/config/config.go index 8800eb9..988fbea 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -77,6 +77,7 @@ func DefaultConfig() Config { "backinfo": true, "activation": true, "dellUpdate": true, + "windowsUpdate": true, "network": true, "pcIdentity": true, }, diff --git a/internal/gui/gui.go b/internal/gui/gui.go index b9c8d9a..0418832 100644 --- a/internal/gui/gui.go +++ b/internal/gui/gui.go @@ -14,6 +14,7 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "sync" "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) { - var mw *walk.MainWindow + var ( + mw *walk.MainWindow + countdownLbl *walk.Label + ) ok, errs, skipped := 0, 0, 0 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) + // cancelled by "Zrusit restart" button + cancelReboot := make(chan struct{}) + if err := (MainWindow{ AssignTo: &mw, Title: "xetup \u2014 hotovo", @@ -371,7 +380,7 @@ func donePhase(results []runner.Result) { }, HSeparator{}, ScrollView{ - MinSize: Size{Height: 560}, + MinSize: Size{Height: 510}, Layout: VBox{MarginsZero: true}, Children: rows, }, @@ -381,16 +390,34 @@ func donePhase(results []runner.Result) { Alignment: AlignHCenterVNear, Font: Font{Bold: true}, }, + Label{ + AssignTo: &countdownLbl, + Text: fmt.Sprintf("Restart za %ds...", rebootCountdown), + Alignment: AlignHCenterVNear, + }, Composite{ Layout: HBox{MarginsZero: true}, Children: []Widget{ HSpacer{}, PushButton{ - Text: " ZAVRIT ", + Text: " Restartovat ted ", OnClicked: func() { + close(cancelReboot) // signal goroutine to stop ticker + reboot() mw.Close() }, }, + PushButton{ + Text: " Zrusit restart ", + OnClicked: func() { + select { + case <-cancelReboot: // already closed + default: + close(cancelReboot) + } + countdownLbl.SetText("Restart zrusen.") + }, + }, HSpacer{}, }, }, @@ -399,9 +426,42 @@ func donePhase(results []runner.Result) { 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() } +// reboot issues an immediate Windows restart. +func reboot() { + exec.Command("shutdown", "/r", "/t", "0").Run() //nolint:errcheck +} + // -------------------------------------------------------------------------- // Helpers // -------------------------------------------------------------------------- diff --git a/internal/runner/hidecmd_other.go b/internal/runner/hidecmd_other.go new file mode 100644 index 0000000..a41ac38 --- /dev/null +++ b/internal/runner/hidecmd_other.go @@ -0,0 +1,7 @@ +//go:build !windows + +package runner + +import "os/exec" + +func hideWindow(cmd *exec.Cmd) {} diff --git a/internal/runner/hidecmd_windows.go b/internal/runner/hidecmd_windows.go new file mode 100644 index 0000000..30e7fd8 --- /dev/null +++ b/internal/runner/hidecmd_windows.go @@ -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} +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go index e073ce3..4bec1d8 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -36,6 +36,7 @@ func AllSteps() []Step { {ID: "backinfo", Num: "07", Name: "BackInfo", ScriptName: "07-backinfo.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: "windowsUpdate", Num: "12", Name: "Windows Update", ScriptName: "12-windows-update.ps1"}, {ID: "network", Num: "09", Name: "Network discovery", ScriptName: "10-network.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...) + hideWindow(cmd) // prevent PS console window from appearing over the GUI stdout, err := cmd.StdoutPipe() if err != nil { @@ -236,6 +238,9 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error scanner := bufio.NewScanner(stdout) for scanner.Scan() { line := scanner.Text() + if skipPSNoiseLine(line) { + continue + } r.onLog(LogLine{ StepID: step.ID, Text: line, @@ -246,6 +251,33 @@ func (r *Runner) runScript(ctx context.Context, step Step, cfgArg string) error 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". func parseLevel(line string) string { if strings.Contains(line, "] [OK]") { diff --git a/scripts/00-admin-account.ps1 b/scripts/00-admin-account.ps1 index bca037d..82d9bb3 100644 --- a/scripts/00-admin-account.ps1 +++ b/scripts/00-admin-account.ps1 @@ -27,6 +27,7 @@ $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 } diff --git a/scripts/01-bloatware.ps1 b/scripts/01-bloatware.ps1 index 7b45b67..fe9108b 100644 --- a/scripts/01-bloatware.ps1 +++ b/scripts/01-bloatware.ps1 @@ -23,6 +23,7 @@ $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 } diff --git a/scripts/02-software.ps1 b/scripts/02-software.ps1 index d6176f5..3385db3 100644 --- a/scripts/02-software.ps1 +++ b/scripts/02-software.ps1 @@ -28,6 +28,7 @@ $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 } diff --git a/scripts/03-system-registry.ps1 b/scripts/03-system-registry.ps1 index 8f7e290..f62d5aa 100644 --- a/scripts/03-system-registry.ps1 +++ b/scripts/03-system-registry.ps1 @@ -37,6 +37,7 @@ $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 } diff --git a/scripts/04-default-profile.ps1 b/scripts/04-default-profile.ps1 index fd08f7b..40d8eee 100644 --- a/scripts/04-default-profile.ps1 +++ b/scripts/04-default-profile.ps1 @@ -36,6 +36,7 @@ $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 } diff --git a/scripts/05-personalization.ps1 b/scripts/05-personalization.ps1 index 9d69fd3..16b79c8 100644 --- a/scripts/05-personalization.ps1 +++ b/scripts/05-personalization.ps1 @@ -26,6 +26,7 @@ $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 } diff --git a/scripts/06-scheduled-tasks.ps1 b/scripts/06-scheduled-tasks.ps1 index ca8c879..304c6ee 100644 --- a/scripts/06-scheduled-tasks.ps1 +++ b/scripts/06-scheduled-tasks.ps1 @@ -23,6 +23,7 @@ $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 } diff --git a/scripts/07-backinfo.ps1 b/scripts/07-backinfo.ps1 index 1303165..eacae46 100644 --- a/scripts/07-backinfo.ps1 +++ b/scripts/07-backinfo.ps1 @@ -25,6 +25,7 @@ $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 } diff --git a/scripts/07-desktop-info.ps1 b/scripts/07-desktop-info.ps1 index e188773..5aa6f7f 100644 --- a/scripts/07-desktop-info.ps1 +++ b/scripts/07-desktop-info.ps1 @@ -25,6 +25,7 @@ $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 } diff --git a/scripts/08-activation.ps1 b/scripts/08-activation.ps1 index 8a87281..e8c8013 100644 --- a/scripts/08-activation.ps1 +++ b/scripts/08-activation.ps1 @@ -26,6 +26,7 @@ $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 } diff --git a/scripts/09-pc-identity.ps1 b/scripts/09-pc-identity.ps1 index b353fac..17f71c3 100644 --- a/scripts/09-pc-identity.ps1 +++ b/scripts/09-pc-identity.ps1 @@ -26,6 +26,7 @@ $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 } diff --git a/scripts/10-network.ps1 b/scripts/10-network.ps1 index e453ec0..3c33582 100644 --- a/scripts/10-network.ps1 +++ b/scripts/10-network.ps1 @@ -24,6 +24,7 @@ $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 } diff --git a/scripts/11-dell-update.ps1 b/scripts/11-dell-update.ps1 index ed17d67..b4928fd 100644 --- a/scripts/11-dell-update.ps1 +++ b/scripts/11-dell-update.ps1 @@ -26,6 +26,7 @@ $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 switch ($Level) { "OK" { Write-Host $line -ForegroundColor Green } diff --git a/scripts/12-windows-update.ps1 b/scripts/12-windows-update.ps1 new file mode 100644 index 0000000..6752775 --- /dev/null +++ b/scripts/12-windows-update.ps1 @@ -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 diff --git a/web/index.html b/web/index.html index 06eef9e..089e7e2 100644 --- a/web/index.html +++ b/web/index.html @@ -178,7 +178,7 @@
curl -Lo xetup.exe xetup.x9.cz/dl
+ curl -Lo xetup.exe https://xetup.x9.cz/dl