Removed MSI

Creating Release instead
This commit is contained in:
2026-03-27 12:20:31 -04:00
parent a77d8dbbf8
commit 3a77ed856e
39 changed files with 141890 additions and 0 deletions
+862
View File
@@ -0,0 +1,862 @@
# ──────────────────────────────────────────────────────────────────────────────
# __ ______ __ _____ ____ __
# / / / / __ \/ / / _/ | / / //_/
# / / / / /_/ / / / // |/ / ,<
# / /_/ / ____/ /____/ // /| / /| |
# \____/_/ /_____/___/_/ |_/_/ |_|
#
#
# __ ______ _ _____ ________________
# / |/ / | / | / / | / ____/ ____/ __ \
# / /|_/ / /| | / |/ / /| |/ / __/ __/ / /_/ /
# / / / / ___ |/ /| / ___ / /_/ / /___/ _, _/
# /_/ /_/_/ |_/_/ |_/_/ |_\____/_____/_/ |_|
#
# ──────────────────────────────────────────────────────────────────────────────
# Uplink Manager Installer
# Author: Sam Jage
# ──────────────────────────────────────────────────────────────────────────────
# ========== SELFELEVATION BLOCK ==========
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Start-Process powershell.exe -Verb RunAs -ArgumentList "-WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
exit
}
# =========================================
# Force 64bit execution if running in 32bit mode
if ($env:PROCESSOR_ARCHITECTURE -ne "AMD64") {
$powershell = "$env:WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
Start-Process $powershell -Verb RunAs -ArgumentList "-WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
exit
}
# =========================================
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Windows.Forms
# ── Version guard ─────────────────────────────────────────────────────────────
if ($PSVersionTable.PSVersion.Major -lt 5) {
[System.Windows.MessageBox]::Show(
"This installer requires PowerShell 5.1 or higher.`n`nCurrent version: $($PSVersionTable.PSVersion)",
"Incompatible PowerShell Version",
[System.Windows.MessageBoxButton]::OK,
[System.Windows.MessageBoxImage]::Error
)
exit 1
}
# ── Colors ────────────────────────────────────────────────────────────────────
$BG = "#252830"
$SURFACE = "#2e3038"
$PANEL = "#404450"
$YELLOW = "#fabd2f"
$GREEN = "#3d5c3f"
$GREEN_LT = "#5a7d5c"
$RED = "#fb4934"
$TEXT = "#ebdbb2"
$TEXT_MUTED = "#a89984"
# ── State ─────────────────────────────────────────────────────────────────────
$script:SelectedInternet = $null
$script:SelectedUplink = $null
$script:CurrentPage = 0
$script:NeedsReboot = $false
$script:InstallDir = "C:\Program Files\Uplink Manager" # adjust if you use x86
$script:SpinnerTimer = $null
$script:SpinnerFrames = @("", "", "", "", "", "", "", "")
$script:SpinnerIndex = 0
$script:ReqJob = $null
$script:PrereqJob = $null
# ── Helpers (unchanged from your original) ──
function Get-Vmxnet3Adapters {
Get-NetAdapter | Where-Object { $_.InterfaceDescription -like "*vmxnet3*" -and $_.Status -eq "Up" } |
Select-Object -ExpandProperty Name
}
function Start-BtnSpinner {
if ($script:SpinnerTimer) { $script:SpinnerTimer.Stop() }
$script:SpinnerIndex = 0
$script:SpinnerTimer = [System.Windows.Threading.DispatcherTimer]::new()
$script:SpinnerTimer.Interval = [TimeSpan]::FromMilliseconds(100)
$script:SpinnerTimer.Add_Tick({
$script:SpinnerIndex = ($script:SpinnerIndex + 1) % $script:SpinnerFrames.Count
$BtnNext.Content = $script:SpinnerFrames[$script:SpinnerIndex]
})
$BtnNext.IsEnabled = $false
$script:SpinnerTimer.Start()
}
function Stop-BtnSpinner {
if ($script:SpinnerTimer) { $script:SpinnerTimer.Stop(); $script:SpinnerTimer = $null }
$BtnNext.IsEnabled = $true
$BtnNext.Content = "Next →"
}
function Test-NatClass {
try { Get-CimClass -Namespace root/StandardCimv2 -ClassName MSFT_NetNat -ErrorAction Stop | Out-Null; return $true }
catch { return $false }
}
# ── XAML UI ───────────────────────────────────────────────────────────────────
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Uplink Manager Installer" Width="680" Height="580"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize"
Background="$BG" FontFamily="Consolas">
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.75"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="StepLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="#a89984"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Margin" Value="0,2,0,2"/>
</Style>
<Style x:Key="StatusDot" TargetType="Ellipse">
<Setter Property="Width" Value="10"/>
<Setter Property="Height" Value="10"/>
<Setter Property="Margin" Value="0,0,8,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</Window.Resources>
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="70"/>
<RowDefinition Height="*"/>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="#1a1c24">
<StackPanel VerticalAlignment="Center" Margin="24,0,0,0">
<TextBlock Text="Uplink Manager" FontSize="20" FontWeight="Bold" Foreground="$YELLOW" FontFamily="Consolas"/>
<TextBlock Text="Virtual Machine Setup Installer for Windows 11" FontSize="11" Foreground="$TEXT_MUTED" Margin="0,2,0,0"/>
</StackPanel>
</Border>
<Grid Grid.Row="1" x:Name="PageContainer" Margin="0"/>
<Border Grid.Row="2" Background="#1a1c24">
<Grid Margin="24,0">
<TextBlock x:Name="FooterStatus" VerticalAlignment="Center" Foreground="$TEXT_MUTED" FontSize="11" Text=""/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="BtnBack" Content=" Back" Width="100" Height="32"
Margin="0,0,8,0" Visibility="Collapsed"
Background="$SURFACE" Foreground="$TEXT" BorderBrush="$PANEL"/>
<Button x:Name="BtnNext" Content="Next " Width="100" Height="32"
Background="$GREEN" Foreground="$TEXT" BorderBrush="$GREEN" FontWeight="Bold"/>
</StackPanel>
</Grid>
</Border>
</Grid>
</Window>
"@
$reader = [System.Xml.XmlNodeReader]::new($xaml)
$window = [System.Windows.Markup.XamlReader]::Load($reader)
# ── Set window icon ───────────────────────────────────────────────────────────
$iconPath = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) "uplink_manager.ico"
if (-not $iconPath -or -not (Test-Path $iconPath)) { $iconPath = Join-Path $PSScriptRoot "uplink_manager.ico" }
if (Test-Path $iconPath) {
$window.Icon = [System.Windows.Media.Imaging.BitmapFrame]::Create([uri]$iconPath)
}
$PageContainer = $window.FindName("PageContainer")
$BtnNext = $window.FindName("BtnNext")
$BtnBack = $window.FindName("BtnBack")
$FooterStatus = $window.FindName("FooterStatus")
# ── UI Helpers ────────────────────────────────────────────────────────────────
function New-SectionTitle {
param([string]$text)
$tb = [System.Windows.Controls.TextBlock]::new()
$tb.Text = $text; $tb.FontSize = 16; $tb.FontWeight = "Bold"
$tb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($YELLOW)
$tb.Margin = [System.Windows.Thickness]::new(0, 0, 0, 8)
return $tb
}
function New-BodyText {
param([string]$text, [object]$color = $TEXT_MUTED)
$tb = [System.Windows.Controls.TextBlock]::new()
$tb.Text = $text; $tb.FontSize = 12; $tb.TextWrapping = "Wrap"
if ($color -is [System.Windows.Media.Brush]) {
$tb.Foreground = $color
}
else {
$tb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString([string]$color)
}
$tb.Margin = [System.Windows.Thickness]::new(0, 0, 0, 6)
return $tb
}
function New-Separator {
$sep = [System.Windows.Controls.Separator]::new()
$sep.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($PANEL)
$sep.Margin = [System.Windows.Thickness]::new(0, 8, 0, 12)
return $sep
}
function New-CheckRow {
param([string]$label, [bool]$passed, [string]$detail = "")
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Orientation = "Horizontal"; $sp.Margin = [System.Windows.Thickness]::new(0, 4, 0, 4)
$dot = [System.Windows.Shapes.Ellipse]::new()
$dot.Width = 10; $dot.Height = 10; $dot.Margin = [System.Windows.Thickness]::new(0, 0, 10, 0); $dot.VerticalAlignment = "Center"
$dot.Fill = [System.Windows.Media.BrushConverter]::new().ConvertFromString($(if ($passed) { $GREEN_LT } else { $RED }))
$sp.Children.Add($dot) | Out-Null
$tb = [System.Windows.Controls.TextBlock]::new()
$tb.Text = "$(if ($passed) { '✔' } else { '✘' }) $label"; $tb.FontSize = 12; $tb.VerticalAlignment = "Center"
$tb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT)
$sp.Children.Add($tb) | Out-Null
if ($detail) {
$dtb = [System.Windows.Controls.TextBlock]::new()
$dtb.Text = " - $detail"; $dtb.FontSize = 11; $dtb.VerticalAlignment = "Center"
$dtb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT_MUTED)
$sp.Children.Add($dtb) | Out-Null
}
return $sp
}
function New-StyledListBox {
param([string[]]$items)
$lb = [System.Windows.Controls.ListBox]::new()
$lb.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#2e3038")
$lb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#ebdbb2")
$lb.BorderBrush = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#404450")
$lb.BorderThickness = [System.Windows.Thickness]::new(1)
$lb.FontFamily = [System.Windows.Media.FontFamily]::new("Consolas")
$lb.FontSize = 12; $lb.Height = 80
$lb.Margin = [System.Windows.Thickness]::new(0, 4, 0, 12)
[System.Windows.Controls.ScrollViewer]::SetHorizontalScrollBarVisibility($lb, [System.Windows.Controls.ScrollBarVisibility]::Disabled)
$itemStyle = [System.Windows.Style]::new([System.Windows.Controls.ListBoxItem])
$bgS = [System.Windows.Setter]::new(); $bgS.Property = [System.Windows.Controls.Control]::BackgroundProperty; $bgS.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#2e3038"); $itemStyle.Setters.Add($bgS)
$fgS = [System.Windows.Setter]::new(); $fgS.Property = [System.Windows.Controls.Control]::ForegroundProperty; $fgS.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#ebdbb2"); $itemStyle.Setters.Add($fgS)
$pdS = [System.Windows.Setter]::new(); $pdS.Property = [System.Windows.Controls.Control]::PaddingProperty; $pdS.Value = [System.Windows.Thickness]::new(8, 4, 8, 4); $itemStyle.Setters.Add($pdS)
$hvT = [System.Windows.Trigger]::new(); $hvT.Property = [System.Windows.Controls.ListBoxItem]::IsMouseOverProperty; $hvT.Value = $true
$hvBg = [System.Windows.Setter]::new(); $hvBg.Property = [System.Windows.Controls.Control]::BackgroundProperty; $hvBg.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#fabd2f")
$hvFg = [System.Windows.Setter]::new(); $hvFg.Property = [System.Windows.Controls.Control]::ForegroundProperty; $hvFg.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#1e1e1e")
$hvT.Setters.Add($hvBg); $hvT.Setters.Add($hvFg); $itemStyle.Triggers.Add($hvT)
$slT = [System.Windows.Trigger]::new(); $slT.Property = [System.Windows.Controls.ListBoxItem]::IsSelectedProperty; $slT.Value = $true
$slBg = [System.Windows.Setter]::new(); $slBg.Property = [System.Windows.Controls.Control]::BackgroundProperty; $slBg.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#3d5c3f")
$slFg = [System.Windows.Setter]::new(); $slFg.Property = [System.Windows.Controls.Control]::ForegroundProperty; $slFg.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#ebdbb2")
$slT.Setters.Add($slBg); $slT.Setters.Add($slFg); $itemStyle.Triggers.Add($slT)
$suT = [System.Windows.MultiTrigger]::new()
$sc1 = [System.Windows.Condition]::new(); $sc1.Property = [System.Windows.Controls.ListBoxItem]::IsSelectedProperty; $sc1.Value = $true
$sc2 = [System.Windows.Condition]::new(); $sc2.Property = [System.Windows.Controls.ListBoxItem]::IsKeyboardFocusWithinProperty; $sc2.Value = $false
$suT.Conditions.Add($sc1); $suT.Conditions.Add($sc2)
$suBg = [System.Windows.Setter]::new(); $suBg.Property = [System.Windows.Controls.Control]::BackgroundProperty; $suBg.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#3d5c3f")
$suFg = [System.Windows.Setter]::new(); $suFg.Property = [System.Windows.Controls.Control]::ForegroundProperty; $suFg.Value = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#ebdbb2")
$suT.Setters.Add($suBg); $suT.Setters.Add($suFg); $itemStyle.Triggers.Add($suT)
$lb.ItemContainerStyle = $itemStyle
foreach ($item in $items) { $lb.Items.Add($item) | Out-Null }
return $lb
}
function Add-PassRow {
param($panel, [string]$text)
$row = [System.Windows.Controls.StackPanel]::new(); $row.Orientation = "Horizontal"; $row.Margin = [System.Windows.Thickness]::new(0, 4, 0, 4)
$greenBrush = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN_LT)
$icon = [System.Windows.Controls.TextBlock]::new(); $icon.Text = ""; $icon.FontSize = 14; $icon.FontWeight = "Bold"
$icon.Foreground = $greenBrush; $icon.Margin = [System.Windows.Thickness]::new(0, 0, 10, 0); $icon.VerticalAlignment = "Center"; $row.Children.Add($icon) | Out-Null
$txt = [System.Windows.Controls.TextBlock]::new(); $txt.Text = $text; $txt.FontSize = 12; $txt.VerticalAlignment = "Center"
$txt.Foreground = $greenBrush; $row.Children.Add($txt) | Out-Null
$panel.Children.Add($row) | Out-Null
}
function Add-FailBlock {
param($panel, [string]$title, [string[]]$hints)
$block = [System.Windows.Controls.StackPanel]::new(); $block.Orientation = "Vertical"; $block.Margin = [System.Windows.Thickness]::new(0, 4, 0, 8)
$hdr = [System.Windows.Controls.StackPanel]::new(); $hdr.Orientation = "Horizontal"
$icon = [System.Windows.Controls.TextBlock]::new(); $icon.Text = "🛑"; $icon.FontSize = 14; $icon.Margin = [System.Windows.Thickness]::new(0, 0, 10, 0); $icon.VerticalAlignment = "Center"; $hdr.Children.Add($icon) | Out-Null
$ttl = [System.Windows.Controls.TextBlock]::new(); $ttl.Text = $title; $ttl.FontSize = 12; $ttl.FontWeight = "Bold"; $ttl.VerticalAlignment = "Center"
$ttl.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($RED); $hdr.Children.Add($ttl) | Out-Null
$block.Children.Add($hdr) | Out-Null
foreach ($hint in $hints) {
$h = [System.Windows.Controls.TextBlock]::new(); $h.Text = " $hint"; $h.FontSize = 11; $h.Margin = [System.Windows.Thickness]::new(24, 2, 0, 0)
$h.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT_MUTED); $block.Children.Add($h) | Out-Null
}
$panel.Children.Add($block) | Out-Null
}
# ── Page 0: Welcome ───────────────────────────────────────────────────────────
function Show-PageWelcome {
$PageContainer.Children.Clear()
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Margin = [System.Windows.Thickness]::new(32, 24, 32, 0)
$sp.Children.Add((New-SectionTitle "Welcome to Uplink Manager Setup")) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$sp.Children.Add((New-BodyText "This installer will configure your Windows 11 Pro VM to run Uplink Manager — a tool for engineers to set up network address translation from an internet-facing adapter to a downstream NAT Uplink adapter.")) | Out-Null
$sp.Children.Add((New-BodyText "The following will be configured on this VM:")) | Out-Null
foreach ($step in @(
"Rename selected adapters to Internet VLAN and NAT Uplink",
"Enable Hyper-V Services (required for WinNAT)",
"Register Windows NAT WMI provider (netttcim.dll)",
"Install Uplink Manager to $($script:InstallDir)",
"Create desktop shortcut"
)) {
$row = [System.Windows.Controls.StackPanel]::new(); $row.Orientation = "Horizontal"; $row.Margin = [System.Windows.Thickness]::new(8, 3, 0, 3)
$dot = [System.Windows.Shapes.Ellipse]::new(); $dot.Width = 6; $dot.Height = 6; $dot.Margin = [System.Windows.Thickness]::new(0, 0, 10, 0); $dot.VerticalAlignment = "Center"
$dot.Fill = [System.Windows.Media.BrushConverter]::new().ConvertFromString($YELLOW); $row.Children.Add($dot) | Out-Null
$tb = [System.Windows.Controls.TextBlock]::new(); $tb.Text = $step; $tb.FontSize = 12
$tb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT); $row.Children.Add($tb) | Out-Null
$sp.Children.Add($row) | Out-Null
}
$sp.Children.Add((New-Separator)) | Out-Null
$sp.Children.Add((New-BodyText "Before continuing, ensure this VM has at least 2 vmxnet3 adapters and hardware virtualization (Expose VT-x to guest) is enabled in VM processor settings." $TEXT_MUTED)) | Out-Null
$PageContainer.Children.Add($sp) | Out-Null
$BtnBack.Visibility = "Collapsed"
$BtnNext.Content = "Begin Setup →"; $BtnNext.IsEnabled = $true
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN)
$FooterStatus.Text = "Step 1 of 6"
}
# ── Page 1: Pre-Installation Requirements ────────────────────────────────────
function Show-PageRequirements {
$PageContainer.Children.Clear()
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Margin = [System.Windows.Thickness]::new(32, 24, 32, 0)
$sp.Children.Add((New-SectionTitle "Pre-Installation Requirements")) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$sp.Children.Add((New-BodyText "Checking VM configuration...")) | Out-Null
$script:ReqResultsPanel = [System.Windows.Controls.StackPanel]::new()
$sp.Children.Add($script:ReqResultsPanel) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$script:ReqWarning = New-BodyText "" $RED
$sp.Children.Add($script:ReqWarning) | Out-Null
$PageContainer.Children.Add($sp) | Out-Null
$BtnBack.Visibility = "Visible"; $BtnNext.IsEnabled = $false
$FooterStatus.Text = "Step 2 of 6"
$script:ReqJob = Start-Job -ScriptBlock {
$virtOk = $false
# Check if a hypervisor is present (most reliable)
try {
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
if ($cs.HypervisorPresent -eq $true) { $virtOk = $true }
}
catch {}
# If not detected, check if HyperV feature is installed
if (-not $virtOk) {
try {
$hyperV = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Platform -ErrorAction SilentlyContinue
if ($hyperV.State -eq "Enabled") { $virtOk = $true }
}
catch {}
}
# Optionally check VirtualizationBased Security registry (indicates hypervisor)
if (-not $virtOk) {
try {
$reg = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard" -Name "EnableVirtualizationBasedSecurity" -ErrorAction SilentlyContinue
if ($reg.EnableVirtualizationBasedSecurity -eq 1) { $virtOk = $true }
}
catch {}
}
$adapters = @(Get-NetAdapter | Where-Object { $_.InterfaceDescription -like "*vmxnet3*" -and $_.Status -eq "Up" } | Select-Object -ExpandProperty Name)
[PSCustomObject]@{ VirtOk = $virtOk; Adapters = $adapters; AdapterOk = $adapters.Count -ge 2 }
}
$window.Dispatcher.Invoke({
$script:timerTickReq = 0
$script:pollTimerReq = [System.Windows.Threading.DispatcherTimer]::new()
$script:pollTimerReq.Interval = [TimeSpan]::FromMilliseconds(400)
$script:pollTimerReq.Tag = $script:ReqJob
$script:pollTimerReq.Add_Tick({
$script:timerTickReq++
if ($script:timerTickReq -gt 38) {
$script:pollTimerReq.Stop()
$script:ReqWarning.Text = "⚠ Check timed out after ~15 seconds. Please restart the installer."
Stop-BtnSpinner; $BtnNext.IsEnabled = $false; return
}
$job = $script:pollTimerReq.Tag
if ($null -eq $job) {
$script:pollTimerReq.Stop()
$script:ReqWarning.Text = "⚠ Background check could not be tracked. Please restart the installer."
Stop-BtnSpinner; $BtnNext.IsEnabled = $false; return
}
if ($job.State -in "Completed", "Failed", "Stopped") {
$script:pollTimerReq.Stop()
$result = $null
try { $result = Receive-Job -Job $job -ErrorAction Stop }
catch { $script:ReqWarning.Text = "⚠ Failed to retrieve job result: $($_.Exception.Message)" }
finally { Remove-Job -Job $job -Force -ErrorAction SilentlyContinue; $script:ReqJob = $null; $script:pollTimerReq.Tag = $null }
if ($null -eq $result) {
$script:ReqWarning.Text = "⚠ Background check returned no data. Please restart the installer."
Stop-BtnSpinner; $BtnNext.IsEnabled = $false
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($PANEL)
}
else {
if ($result.VirtOk) {
Add-PassRow $script:ReqResultsPanel "Hardware virtualization exposed to guest OS"
}
else {
Add-FailBlock $script:ReqResultsPanel "Hardware virtualization not detected" @(
"— Shutdown this VM",
"— In VMware: VM Settings → Processors",
"— Enable: Expose hardware assisted virtualization to the guest OS"
)
}
if ($result.AdapterOk) {
Add-PassRow $script:ReqResultsPanel "vmxnet3 adapters detected — $($result.Adapters.Count) found"
}
else {
Add-FailBlock $script:ReqResultsPanel "Insufficient vmxnet3 adapters — $($result.Adapters.Count) found (minimum 2 required)" @(
"— Shutdown this VM",
"— In VMware: VM Settings → Network Adapters",
"— Add at least 2 vmxnet3 network adapters"
)
}
$listBlock = [System.Windows.Controls.StackPanel]::new(); $listBlock.Margin = [System.Windows.Thickness]::new(24, 4, 0, 0)
foreach ($adapter in $result.Adapters) {
$r = [System.Windows.Controls.TextBlock]::new(); $r.Text = "· $adapter"; $r.FontSize = 11; $r.Margin = [System.Windows.Thickness]::new(0, 2, 0, 0)
$r.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT_MUTED); $listBlock.Children.Add($r) | Out-Null
}
$script:ReqResultsPanel.Children.Add($listBlock) | Out-Null
$allOk = $result.VirtOk -and $result.AdapterOk
if (-not $allOk) { $script:ReqWarning.Text = "⚠ Resolve the issues above before continuing. Shutdown the VM if required." }
Stop-BtnSpinner
if (-not $allOk) {
$BtnNext.IsEnabled = $false
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($PANEL)
}
else {
$BtnNext.IsEnabled = $true; $BtnNext.Content = "Next →"
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN)
}
}
}
})
$script:pollTimerReq.Start()
})
}
# ── Page 2: VM Prerequisites ──────────────────────────────────────────────────
function Show-PagePrereqs {
$PageContainer.Children.Clear()
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Margin = [System.Windows.Thickness]::new(32, 24, 32, 0)
$sp.Children.Add((New-SectionTitle "VM Prerequisites Check")) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$sp.Children.Add((New-BodyText "Verifying this VM meets requirements for Uplink Manager...")) | Out-Null
$script:PrereqResultsPanel = [System.Windows.Controls.StackPanel]::new()
$sp.Children.Add($script:PrereqResultsPanel) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$script:PrereqWarning = New-BodyText "" $RED
$sp.Children.Add($script:PrereqWarning) | Out-Null
$PageContainer.Children.Add($sp) | Out-Null
$BtnBack.Visibility = "Visible"; $BtnNext.IsEnabled = $false
$FooterStatus.Text = "Step 3 of 6"
$script:PrereqJob = Start-Job -ScriptBlock {
$adminOk = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
$osVer = [System.Environment]::OSVersion.Version
$winOk = $osVer.Major -ge 10
$adapters = @(Get-NetAdapter | Where-Object { $_.InterfaceDescription -like "*vmxnet3*" -and $_.Status -eq "Up" } | Select-Object -ExpandProperty Name)
$adapterOk = $adapters.Count -ge 2
# Reliable virtualization detection
$virtOk = $false
try {
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
if ($cs.HypervisorPresent -eq $true) { $virtOk = $true }
}
catch {}
if (-not $virtOk) {
try {
$hyperV = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Platform -ErrorAction SilentlyContinue
if ($hyperV.State -eq "Enabled") { $virtOk = $true }
}
catch {}
}
if (-not $virtOk) {
try {
$reg = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\DeviceGuard" -Name "EnableVirtualizationBasedSecurity" -ErrorAction SilentlyContinue
if ($reg.EnableVirtualizationBasedSecurity -eq 1) { $virtOk = $true }
}
catch {}
}
[PSCustomObject]@{ AdminOk = $adminOk; WinOk = $winOk; OsBuild = $osVer.Build; AdapterOk = $adapterOk; AdCount = $adapters.Count; VirtOk = $virtOk }
}
$window.Dispatcher.Invoke({
$script:timerTickPrereq = 0
$script:pollTimerPrereq = [System.Windows.Threading.DispatcherTimer]::new()
$script:pollTimerPrereq.Interval = [TimeSpan]::FromMilliseconds(400)
$script:pollTimerPrereq.Tag = $script:PrereqJob
$script:pollTimerPrereq.Add_Tick({
$script:timerTickPrereq++
if ($script:timerTickPrereq -gt 38) {
$script:pollTimerPrereq.Stop()
$script:PrereqWarning.Text = "⚠ Prerequisite check timed out after ~15 seconds."
Stop-BtnSpinner; $BtnNext.IsEnabled = $false; return
}
$job = $script:pollTimerPrereq.Tag
if ($null -eq $job) {
$script:pollTimerPrereq.Stop()
$script:PrereqWarning.Text = "⚠ Prerequisite check could not be tracked. Please restart the installer."
Stop-BtnSpinner; $BtnNext.IsEnabled = $false; return
}
if ($job.State -in "Completed", "Failed", "Stopped") {
$script:pollTimerPrereq.Stop()
$r = $null
try { $r = Receive-Job -Job $job -ErrorAction Stop }
catch { $script:PrereqWarning.Text = "⚠ Failed to retrieve prerequisite check result: $($_.Exception.Message)" }
finally { Remove-Job -Job $job -Force -ErrorAction SilentlyContinue; $script:PrereqJob = $null; $script:pollTimerPrereq.Tag = $null }
if ($null -eq $r) {
$script:PrereqWarning.Text = "⚠ Prerequisite check returned no data. Please restart the installer."
Stop-BtnSpinner; $BtnNext.IsEnabled = $false
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($PANEL)
}
else {
$script:PrereqResultsPanel.Children.Add((New-CheckRow "Running as Administrator" $r.AdminOk)) | Out-Null
$script:PrereqResultsPanel.Children.Add((New-CheckRow "Windows 10/11 detected" $r.WinOk "Build $($r.OsBuild)")) | Out-Null
$script:PrereqResultsPanel.Children.Add((New-CheckRow "At least 2 vmxnet3 adapters present" $r.AdapterOk "$($r.AdCount) found")) | Out-Null
$script:PrereqResultsPanel.Children.Add((New-CheckRow "Hardware virtualization exposed to VM" $r.VirtOk $(if (-not $r.VirtOk) { "Enable in VMware VM Settings → Processors" } else { "" }))) | Out-Null
$allOk = $r.AdminOk -and $r.WinOk -and $r.AdapterOk
if (-not $allOk) {
$script:PrereqWarning.Text = "⚠ One or more checks failed. Please resolve the issues above before continuing."
}
else {
$script:PrereqResultsPanel.Children.Add((New-BodyText "✔ All critical checks passed. You may continue." $GREEN_LT)) | Out-Null
}
Stop-BtnSpinner
if (-not $allOk) {
$BtnNext.IsEnabled = $false
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($PANEL)
}
else {
$BtnNext.IsEnabled = $true; $BtnNext.Content = "Next →"
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN)
}
}
}
})
$script:pollTimerPrereq.Start()
})
}
# ── Page 3: Adapter selection ─────────────────────────────────────────────────
function Show-PageAdapters {
$PageContainer.Children.Clear()
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Margin = [System.Windows.Thickness]::new(32, 24, 32, 0)
$sp.Children.Add((New-SectionTitle "Configure Network Adapters")) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$sp.Children.Add((New-BodyText "Select which adapter serves each role. They will be renamed to match what Uplink Manager expects.")) | Out-Null
$adapters = Get-Vmxnet3Adapters
$tb1 = New-BodyText "Internet-facing adapter (WAN uplink — receives internet from your network)"
$tb1.FontWeight = "Bold"; $tb1.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT); $sp.Children.Add($tb1) | Out-Null
$script:ComboInternet = New-StyledListBox $adapters
if ($script:SelectedInternet) { $script:ComboInternet.SelectedItem = $script:SelectedInternet }
$sp.Children.Add($script:ComboInternet) | Out-Null
$tb2 = New-BodyText "NAT Uplink adapter (downstream — connects to devices that need internet)"
$tb2.FontWeight = "Bold"; $tb2.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT); $sp.Children.Add($tb2) | Out-Null
$script:ComboUplink = New-StyledListBox $adapters
if ($script:SelectedUplink) { $script:ComboUplink.SelectedItem = $script:SelectedUplink }
$sp.Children.Add($script:ComboUplink) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$script:AdapterWarning = New-BodyText "" $RED; $sp.Children.Add($script:AdapterWarning) | Out-Null
$sp.Children.Add((New-BodyText "After selection, these adapters will be renamed: Internet VLAN and NAT Uplink" $TEXT_MUTED)) | Out-Null
$PageContainer.Children.Add($sp) | Out-Null
$BtnBack.Visibility = "Visible"
$BtnNext.Content = "Next →"; $BtnNext.IsEnabled = $true
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN)
$FooterStatus.Text = "Step 4 of 6"
Stop-BtnSpinner
}
# ── Page 4: Confirm ───────────────────────────────────────────────────────────
function Show-PageConfirm {
$PageContainer.Children.Clear()
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Margin = [System.Windows.Thickness]::new(32, 24, 32, 0)
$sp.Children.Add((New-SectionTitle "Confirm Installation")) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$sp.Children.Add((New-BodyText "The following changes will be made to this VM:")) | Out-Null
foreach ($action in @(
"Rename '$($script:SelectedInternet)' → Internet VLAN",
"Rename '$($script:SelectedUplink)' → NAT Uplink",
"Enable Hyper-V Services (may require reboot)",
"Register netttcim.dll WMI provider",
"Compile netttcim.mof WMI class definitions",
"Create desktop shortcut (no arrow overlay)"
)) {
$row = [System.Windows.Controls.StackPanel]::new(); $row.Orientation = "Horizontal"; $row.Margin = [System.Windows.Thickness]::new(8, 4, 0, 4)
$dot = [System.Windows.Shapes.Ellipse]::new(); $dot.Width = 6; $dot.Height = 6; $dot.Margin = [System.Windows.Thickness]::new(0, 0, 10, 0); $dot.VerticalAlignment = "Center"
$dot.Fill = [System.Windows.Media.BrushConverter]::new().ConvertFromString($YELLOW); $row.Children.Add($dot) | Out-Null
$tb = [System.Windows.Controls.TextBlock]::new(); $tb.Text = $action; $tb.FontSize = 12
$tb.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT); $row.Children.Add($tb) | Out-Null
$sp.Children.Add($row) | Out-Null
}
$sp.Children.Add((New-Separator)) | Out-Null
$PageContainer.Children.Add($sp) | Out-Null
$BtnBack.Visibility = "Visible"
$BtnNext.Content = "⚡ Install"; $BtnNext.IsEnabled = $true
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN)
$FooterStatus.Text = "Step 5 of 6"
Stop-BtnSpinner
}
# ── Page 5: Install ───────────────────────────────────────────────────────────
function Show-PageInstall {
$PageContainer.Children.Clear()
$BtnBack.Visibility = "Collapsed"
$sp = [System.Windows.Controls.StackPanel]::new()
$sp.Margin = [System.Windows.Thickness]::new(32, 24, 32, 0)
# Section title that we will update later
$script:installTitle = New-SectionTitle "Installing..."
$sp.Children.Add($script:installTitle) | Out-Null
$sp.Children.Add((New-Separator)) | Out-Null
$script:LogBox = [System.Windows.Controls.TextBlock]::new()
$script:LogBox.FontSize = 11
$script:LogBox.FontFamily = [System.Windows.Media.FontFamily]::new("Consolas")
$script:LogBox.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString($TEXT_MUTED)
$script:LogBox.TextWrapping = "Wrap"; $script:LogBox.Text = ""
$scroll = [System.Windows.Controls.ScrollViewer]::new()
$scroll.Height = 290
$scroll.VerticalScrollBarVisibility = [System.Windows.Controls.ScrollBarVisibility]::Hidden # hides vertical scroll bar
$scroll.HorizontalScrollBarVisibility = [System.Windows.Controls.ScrollBarVisibility]::Disabled # prevents horizontal scroll bar
$scroll.Content = $script:LogBox
$scroll.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($SURFACE)
$scroll.Padding = [System.Windows.Thickness]::new(8)
$sp.Children.Add($scroll) | Out-Null
$PageContainer.Children.Add($sp) | Out-Null
$FooterStatus.Text = "Step 6 of 6 — Installing..."
# Create a temporary log file
$script:LogFilePath = Join-Path $env:TEMP "uplink_install_log_$pid.txt"
if (Test-Path $script:LogFilePath) { Remove-Item $script:LogFilePath -Force }
# Start background job that writes to the log file
$script:InstallJob = Start-Job -Name "UplinkInstall" -ScriptBlock {
param($SelectedInternet, $SelectedUplink, $InstallDir, $LogFilePath)
function Write-LogLine { param($Line) $Line | Out-File -FilePath $LogFilePath -Append -Encoding UTF8 }
$ok = $true
$needsReboot = $false
Write-LogLine "── Configuring Network Adapters ────────────────────"
try {
Rename-NetAdapter -Name $SelectedInternet -NewName "Internet VLAN" -ErrorAction Stop
Write-LogLine " ✔ Renamed '$SelectedInternet' → 'Internet VLAN'"
} catch {
Write-LogLine " ✘ Failed to rename Internet adapter: $_"
$ok = $false
}
try {
Rename-NetAdapter -Name $SelectedUplink -NewName "NAT Uplink" -ErrorAction Stop
Write-LogLine " ✔ Renamed '$SelectedUplink' → 'NAT Uplink'"
} catch {
Write-LogLine " ✘ Failed to rename NAT Uplink adapter: $_"
$ok = $false
}
Write-LogLine "── Enabling Hyper-V Services ────────────────────────"
try {
$proc = Start-Process -FilePath "dism.exe" -ArgumentList "/online /enable-feature /featurename:Microsoft-Hyper-V-Services /all /quiet /norestart" -Wait -PassThru -NoNewWindow
if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq 3010) {
$needsReboot = $true
Write-LogLine " ✔ Hyper-V Services enabled (reboot required)"
} else {
Write-LogLine " ✘ Failed to enable Hyper-V Services, DISM exit code: $($proc.ExitCode)"
$ok = $false
}
} catch {
Write-LogLine " ✘ Failed to enable Hyper-V Services: $_"
$ok = $false
}
Write-LogLine "── Registering WMI NAT Provider ─────────────────────"
$dllPath = "C:\Windows\System32\wbem\netttcim.dll"
$mofPath = "C:\Windows\System32\wbem\netttcim.mof"
if (Test-Path $dllPath) {
try {
$reg = Start-Process -FilePath "regsvr32.exe" -ArgumentList "/s `"$dllPath`"" -Wait -PassThru
if ($reg.ExitCode -eq 0) { Write-LogLine " ✔ Registered netttcim.dll" } else { Write-LogLine " ⚠ regsvr32 returned $($reg.ExitCode)" }
} catch { Write-LogLine " ✘ Failed to register DLL: $_" }
} else { Write-LogLine " ✘ netttcim.dll not found"; $ok = $false }
if (Test-Path $mofPath) {
try {
$mof = Start-Process -FilePath "mofcomp.exe" -ArgumentList "`"$mofPath`"" -Wait -PassThru
if ($mof.ExitCode -eq 0) { Write-LogLine " ✔ Compiled netttcim.mof" } else { Write-LogLine " ⚠ mofcomp returned $($mof.ExitCode)" }
} catch { Write-LogLine " ✘ Failed to compile MOF: $_" }
} else { Write-LogLine " ✘ netttcim.mof not found"; $ok = $false }
try {
Restart-Service winmgmt -Force -ErrorAction SilentlyContinue; Start-Sleep -Seconds 2
try { Get-CimClass -Namespace root/StandardCimv2 -ClassName MSFT_NetNat -ErrorAction Stop | Out-Null; $natOk = $true } catch { $natOk = $false }
if ($natOk) { Write-LogLine " ✔ MSFT_NetNat WMI class verified" }
else { Write-LogLine " ✔ MSFT_NetNat will be available after reboot (expected)"; $needsReboot = $true }
} catch { Write-LogLine " ⚠ Could not restart WMI service" }
Write-LogLine "── Verifying Uplink Manager ────────────────────────────"
$exeDest = Join-Path $InstallDir "Uplink Manager.exe"
if (Test-Path $exeDest) {
Write-LogLine " ✔ Uplink Manager.exe verified in $InstallDir"
} else {
Write-LogLine " ✘ Uplink Manager.exe missing from $InstallDir"
$ok = $false
}
Write-LogLine "── Creating Logs Folder ─────────────────────────────"
$logsDir = Join-Path $InstallDir "logs"
try {
if (-not (Test-Path $logsDir)) {
New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
Write-LogLine " ✔ Created logs folder: $logsDir"
} else {
Write-LogLine " ✔ Logs folder already exists: $logsDir"
}
} catch {
Write-LogLine " ⚠ Could not create logs folder: $_"
}
Write-LogLine "── Creating Desktop Shortcut ────────────────────────"
try {
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Icons"
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
Set-ItemProperty -Path $regPath -Name "29" -Value "%SystemRoot%\System32\imageres.dll,197" -Type String -Force
Write-LogLine " ✔ Shortcut arrow overlay removed"
$shortcutPath = [System.IO.Path]::Combine([Environment]::GetFolderPath("Desktop"), "Uplink Manager.lnk")
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $exeDest
$shortcut.WorkingDirectory = $InstallDir
$shortcut.Description = "Uplink Manager"
$shortcut.Save()
Write-LogLine " ✔ Desktop shortcut created"
Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue; Start-Sleep -Milliseconds 500
} catch { Write-LogLine " ⚠ Shortcut creation issue: $_" }
Write-LogLine ""
if ($ok) {
Write-LogLine "✔ Installation complete!"
if ($needsReboot) { Write-LogLine "⚠ A reboot is required to finalize Hyper-V Services and WMI changes." }
} else {
Write-LogLine "⚠ Installation completed with errors. Review log above."
}
# Mark completion and reboot status
Write-LogLine "___INSTALL_COMPLETE___"
if ($needsReboot) { Write-LogLine "___REBOOT_REQUIRED___" } else { Write-LogLine "___NO_REBOOT___" }
} -ArgumentList $script:SelectedInternet, $script:SelectedUplink, $script:InstallDir, $script:LogFilePath
# Timer to tail the log file
$script:lastFileSize = 0
$script:installPollTimer = [System.Windows.Threading.DispatcherTimer]::new()
$script:installPollTimer.Interval = [TimeSpan]::FromMilliseconds(300)
$script:installPollTimer.Add_Tick({
if (-not (Test-Path $script:LogFilePath)) { return }
$fs = [System.IO.File]::Open($script:LogFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
$fs.Seek($script:lastFileSize, [System.IO.SeekOrigin]::Begin) | Out-Null
$reader = [System.IO.StreamReader]::new($fs)
$newLines = $reader.ReadToEnd()
$script:lastFileSize = $fs.Position
$reader.Close()
$fs.Close()
if ($newLines) {
$script:LogBox.Text += $newLines
$script:LogBox.UpdateLayout()
}
# Check for completion marker
if ($newLines -like "*___INSTALL_COMPLETE___*") {
$script:installPollTimer.Stop()
$job = Get-Job -Name "UplinkInstall" -ErrorAction SilentlyContinue
if ($job) { Receive-Job -Job $job; Remove-Job -Job $job -Force }
# Clean up log file
if (Test-Path $script:LogFilePath) { Remove-Item $script:LogFilePath -Force }
# Determine if reboot is needed from the log
$needsReboot = $newLines -like "*___REBOOT_REQUIRED___*"
$script:NeedsReboot = $needsReboot
# Update UI
Stop-BtnSpinner
if ($needsReboot) {
$script:installTitle.Text = "Installed Please Reboot"
$BtnNext.Content = "Reboot Now"
$FooterStatus.Text = "Done Reboot required"
} else {
$script:installTitle.Text = "Installation Complete"
$BtnNext.Content = "Finish"
$FooterStatus.Text = "Done"
}
$BtnNext.IsEnabled = $true
$BtnNext.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString($GREEN)
$script:CurrentPage = 5
}
})
$script:installPollTimer.Start()
}
# ── Navigation ────────────────────────────────────────────────────────────────
$BtnNext.Add_Click({
Start-BtnSpinner
switch ($script:CurrentPage) {
0 { $script:CurrentPage = 1; Show-PageRequirements }
1 { $script:CurrentPage = 2; Show-PagePrereqs }
2 { $script:CurrentPage = 3; Show-PageAdapters }
3 {
$internet = $script:ComboInternet.SelectedItem
$uplink = $script:ComboUplink.SelectedItem
if (-not $internet -or -not $uplink) {
$script:AdapterWarning.Text = "⚠ Please select both adapters before continuing."
Stop-BtnSpinner; return
}
if ($internet -eq $uplink) {
$script:AdapterWarning.Text = "⚠ Internet and NAT Uplink adapters must be different."
Stop-BtnSpinner; return
}
$script:SelectedInternet = $internet; $script:SelectedUplink = $uplink
$script:CurrentPage = 4; Show-PageConfirm
}
4 { $script:CurrentPage = 5; Show-PageInstall }
5 { if ($script:NeedsReboot) { Restart-Computer -Force } else { $window.Close() } }
}
})
$BtnBack.Add_Click({
switch ($script:CurrentPage) {
1 { $script:CurrentPage = 0; Show-PageWelcome }
2 { $script:CurrentPage = 1; Show-PageRequirements }
3 { $script:CurrentPage = 2; Show-PagePrereqs }
4 { $script:CurrentPage = 3; Show-PageAdapters }
}
})
# ── Launch ────────────────────────────────────────────────────────────────────
Show-PageWelcome
$window.ShowDialog() | Out-Null