Files
Uplink-Manager/uplink_manager_installer.ps1
T
2026-03-27 13:39:56 -04:00

862 lines
50 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ──────────────────────────────────────────────────────────────────────────────
# __ ______ __ _____ ____ __
# / / / / __ \/ / / _/ | / / //_/
# / / / / /_/ / / / // |/ / ,<
# / /_/ / ____/ /____/ // /| / /| |
# \____/_/ /_____/___/_/ |_/_/ |_|
#
#
# __ ______ _ _____ ________________
# / |/ / | / | / / | / ____/ ____/ __ \
# / /|_/ / /| | / |/ / /| |/ / __/ __/ / /_/ /
# / / / / ___ |/ /| / ___ / /_/ / /___/ _, _/
# /_/ /_/_/ |_/_/ |_/_/ |_\____/_____/_/ |_|
#
# ──────────────────────────────────────────────────────────────────────────────
# 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