Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
334 lines
28 KiB
PowerShell
334 lines
28 KiB
PowerShell
[CmdletBinding()]
|
||
param([switch]$SkipProductionDeploy)
|
||
|
||
Set-StrictMode -Version 2.0
|
||
$ErrorActionPreference = 'Stop'
|
||
Add-Type -AssemblyName PresentationFramework
|
||
Add-Type -AssemblyName PresentationCore
|
||
Add-Type -AssemblyName WindowsBase
|
||
Add-Type -AssemblyName System.Xaml
|
||
|
||
$PackageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
|
||
$TargetVersion = '1.43.0'
|
||
$UpdaterVersion = '3.3.0'
|
||
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker.ps1'
|
||
$WorkerHostScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Host.ps1'
|
||
$LocalRoot = Join-Path $env:LOCALAPPDATA 'Vendoo\Updater'
|
||
$RuntimeRoot = Join-Path $LocalRoot 'runtime'
|
||
$SessionRoot = Join-Path $LocalRoot 'sessions'
|
||
New-Item -ItemType Directory -Path $RuntimeRoot,$SessionRoot -Force | Out-Null
|
||
|
||
$createdNew = $false
|
||
$mutex = [System.Threading.Mutex]::new($true, 'Local\VendooProductionUpdater', [ref]$createdNew)
|
||
if (-not $createdNew) {
|
||
[System.Windows.MessageBox]::Show('Der Vendoo-Updater läuft bereits. Bitte das vorhandene Fenster verwenden.','Vendoo Updater',[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Information) | Out-Null
|
||
return
|
||
}
|
||
|
||
$xaml = @'
|
||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||
Title="Vendoo Production Updater" Width="1080" Height="720" MinWidth="960" MinHeight="650"
|
||
WindowStartupLocation="CenterScreen" Background="#0F1115" Foreground="#EEF1F5"
|
||
FontFamily="Segoe UI" ResizeMode="CanResize">
|
||
<Window.Resources>
|
||
<SolidColorBrush x:Key="Panel" Color="#171A20"/>
|
||
<SolidColorBrush x:Key="Panel2" Color="#1D2129"/>
|
||
<SolidColorBrush x:Key="Border" Color="#2B313C"/>
|
||
<SolidColorBrush x:Key="Muted" Color="#9AA4B2"/>
|
||
<SolidColorBrush x:Key="Accent" Color="#4F8CFF"/>
|
||
<Style TargetType="Button">
|
||
<Setter Property="Foreground" Value="#EEF1F5"/><Setter Property="Background" Value="#252B35"/>
|
||
<Setter Property="BorderBrush" Value="#343C49"/><Setter Property="Padding" Value="16,9"/>
|
||
<Setter Property="FontWeight" Value="SemiBold"/><Setter Property="Cursor" Value="Hand"/>
|
||
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button">
|
||
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="7" Padding="{TemplateBinding Padding}">
|
||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||
</Border><ControlTemplate.Triggers>
|
||
<Trigger Property="IsMouseOver" Value="True"><Setter Property="Background" Value="#303744"/></Trigger>
|
||
<Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0.42"/></Trigger>
|
||
</ControlTemplate.Triggers>
|
||
</ControlTemplate></Setter.Value></Setter>
|
||
</Style>
|
||
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||
<Setter Property="Background" Value="#2E6FE8"/><Setter Property="BorderBrush" Value="#4F8CFF"/>
|
||
</Style>
|
||
</Window.Resources>
|
||
<Grid>
|
||
<Grid.RowDefinitions><RowDefinition Height="76"/><RowDefinition Height="*"/><RowDefinition Height="68"/></Grid.RowDefinitions>
|
||
<Border Grid.Row="0" Background="#14171C" BorderBrush="#252B35" BorderThickness="0,0,0,1">
|
||
<Grid Margin="28,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||
<StackPanel VerticalAlignment="Center">
|
||
<TextBlock FontSize="23" FontWeight="SemiBold" Text="Vendoo Production Updater"/>
|
||
<TextBlock Foreground="{StaticResource Muted}" FontSize="12" Margin="0,4,0,0" Text="Sicherer Release-, GitHub- und Coolify-Ablauf"/>
|
||
</StackPanel>
|
||
<Border Grid.Column="1" Background="#202631" BorderBrush="#343C49" BorderThickness="1" CornerRadius="8" Padding="14,8" VerticalAlignment="Center">
|
||
<StackPanel Orientation="Horizontal"><TextBlock Text="ZIEL "/><TextBlock FontWeight="Bold" Text="1.43.0"/></StackPanel>
|
||
</Border>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<Grid Grid.Row="1" Margin="24,22,24,16">
|
||
<Grid.ColumnDefinitions><ColumnDefinition Width="300"/><ColumnDefinition Width="18"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||
<Border Grid.Column="0" Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Padding="18">
|
||
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||
<StackPanel><TextBlock FontSize="15" FontWeight="SemiBold" Text="Update-Ablauf"/><TextBlock Foreground="{StaticResource Muted}" FontSize="11" Margin="0,4,0,14" Text="Jeder Schritt wird einzeln geprüft."/></StackPanel>
|
||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"><StackPanel x:Name="StepsPanel"/></ScrollViewer>
|
||
<Border Grid.Row="2" Background="#12151A" BorderBrush="#272D37" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,14,0,0">
|
||
<StackPanel><TextBlock Foreground="{StaticResource Muted}" FontSize="10" Text="SICHERHEIT"/>
|
||
<TextBlock FontSize="11" Margin="0,5,0,0" TextWrapping="Wrap" Text="Kein Datenbank-Reset. Keine Uploads, Secrets oder /app/data-Dateien werden ersetzt."/>
|
||
</StackPanel>
|
||
</Border>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<Grid Grid.Column="2"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Padding="22">
|
||
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||
<Grid><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||
<StackPanel><TextBlock x:Name="PhaseText" FontSize="20" FontWeight="SemiBold" Text="Updater wird vorbereitet"/>
|
||
<TextBlock x:Name="DetailText" Foreground="{StaticResource Muted}" Margin="0,7,20,0" TextWrapping="Wrap" Text="Die sichere Update-Umgebung wird initialisiert."/>
|
||
</StackPanel>
|
||
<Border x:Name="StatusBadge" Grid.Column="1" Background="#22314A" BorderBrush="#36598A" BorderThickness="1" CornerRadius="7" Padding="12,7" VerticalAlignment="Top">
|
||
<TextBlock x:Name="StatusText" FontWeight="Bold" FontSize="11" Text="START"/>
|
||
</Border>
|
||
</Grid>
|
||
<Grid Grid.Row="1" Margin="0,24,0,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="64"/></Grid.ColumnDefinitions>
|
||
<ProgressBar x:Name="ProgressBar" Height="10" Minimum="0" Maximum="100" Value="0" Foreground="{StaticResource Accent}" Background="#262C35" BorderThickness="0"/>
|
||
<TextBlock x:Name="PercentText" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" FontWeight="SemiBold" Text="0 %"/>
|
||
</Grid>
|
||
<Grid Grid.Row="2" Margin="0,13,0,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||
<TextBlock x:Name="ActivityText" Foreground="{StaticResource Muted}" FontSize="11" Text="Laufzeit 00:00"/>
|
||
<TextBlock x:Name="HeartbeatText" Grid.Column="1" Foreground="{StaticResource Muted}" FontSize="11" Text="Status wird aufgebaut"/>
|
||
</Grid>
|
||
<Border x:Name="InputPanel" Grid.Row="3" Visibility="Collapsed" Background="#171E2A" BorderBrush="#35547E" BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,18,0,0">
|
||
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||
<TextBlock x:Name="InputTitle" FontWeight="SemiBold" Text="Eingabe erforderlich"/>
|
||
<TextBlock x:Name="InputPrompt" Grid.Row="1" Foreground="{StaticResource Muted}" Margin="0,6,0,10" TextWrapping="Wrap"/>
|
||
<Grid Grid.Row="2"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||
<PasswordBox x:Name="SecretInput" Height="34" Padding="8" Background="#0F1217" Foreground="#EEF1F5" BorderBrush="#3A4657"/>
|
||
<Button x:Name="SubmitInputButton" Grid.Column="1" Style="{StaticResource PrimaryButton}" Margin="10,0,0,0" Content="Sicher übernehmen"/>
|
||
</Grid>
|
||
</Grid>
|
||
</Border>
|
||
</Grid>
|
||
</Border>
|
||
|
||
<Border Grid.Row="1" x:Name="ErrorPanel" Visibility="Collapsed" Background="#24181B" BorderBrush="#7A3640" BorderThickness="1" CornerRadius="10" Padding="18" Margin="0,14,0,0">
|
||
<StackPanel><TextBlock x:Name="ErrorTitle" FontSize="15" FontWeight="SemiBold" Foreground="#FFB6BE" Text="Update gestoppt"/>
|
||
<TextBlock x:Name="ErrorText" Margin="0,7,0,0" TextWrapping="Wrap"/>
|
||
<TextBlock x:Name="RemediationText" Foreground="#D7A9AF" Margin="0,8,0,0" TextWrapping="Wrap"/>
|
||
</StackPanel>
|
||
</Border>
|
||
|
||
<Border Grid.Row="2" Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Margin="0,14,0,0">
|
||
<Grid><Grid.RowDefinitions><RowDefinition Height="42"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||
<Grid Margin="16,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="Technisches Protokoll"/>
|
||
<TextBlock x:Name="LogPathText" Grid.Column="1" VerticalAlignment="Center" Foreground="{StaticResource Muted}" FontSize="10" Text="wird vorbereitet"/>
|
||
</Grid>
|
||
<TextBox x:Name="LogBox" Grid.Row="1" Margin="12,0,12,12" IsReadOnly="True" AcceptsReturn="True" TextWrapping="NoWrap" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="#0D0F13" Foreground="#CDD4DE" BorderBrush="#292F38" FontFamily="Consolas" FontSize="11" Padding="10"/>
|
||
</Grid>
|
||
</Border>
|
||
</Grid>
|
||
</Grid>
|
||
|
||
<Border Grid.Row="2" Background="#14171C" BorderBrush="#252B35" BorderThickness="0,1,0,0">
|
||
<Grid Margin="24,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||
<Button x:Name="OpenLogButton" Content="Protokoll öffnen" IsEnabled="False"/>
|
||
<Button x:Name="OpenRuntimeButton" Content="Diagnoseordner" Margin="10,0,0,0" IsEnabled="False"/>
|
||
<Button x:Name="CopyErrorButton" Content="Fehler kopieren" Margin="10,0,0,0" Visibility="Collapsed"/>
|
||
</StackPanel>
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||
<Button x:Name="CancelButton" Content="Update abbrechen"/>
|
||
<Button x:Name="RetryButton" Content="Erneut versuchen" Margin="10,0,0,0" Visibility="Collapsed"/>
|
||
<Button x:Name="CloseButton" Style="{StaticResource PrimaryButton}" Content="Schließen" Margin="10,0,0,0" IsEnabled="False"/>
|
||
</StackPanel>
|
||
</Grid>
|
||
</Border>
|
||
</Grid>
|
||
</Window>
|
||
'@
|
||
|
||
$reader = [System.Xml.XmlNodeReader]::new([xml]$xaml)
|
||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||
$names = @('StepsPanel','PhaseText','DetailText','StatusBadge','StatusText','ProgressBar','PercentText','ActivityText','HeartbeatText','InputPanel','InputTitle','InputPrompt','SecretInput','SubmitInputButton','ErrorPanel','ErrorTitle','ErrorText','RemediationText','LogBox','LogPathText','OpenLogButton','OpenRuntimeButton','CopyErrorButton','CancelButton','RetryButton','CloseButton')
|
||
foreach ($name in $names) { Set-Variable -Name $name -Value $window.FindName($name) -Scope Script }
|
||
|
||
$steps = @(
|
||
@{id='package'; title='Paket prüfen'}, @{id='tools'; title='System prüfen'}, @{id='github'; title='GitHub verbinden'},
|
||
@{id='production'; title='Produktion vorbereiten'}, @{id='workspace'; title='Arbeitskopie erstellen'}, @{id='tests'; title='Vollständig testen'},
|
||
@{id='pullrequest'; title='Pull Request erstellen'}, @{id='merge'; title='Sicher mergen'}, @{id='deploy'; title='Coolify deployen'}, @{id='verify'; title='Produktion bestätigen'}
|
||
)
|
||
$script:StepControls = @{}
|
||
foreach ($step in $steps) {
|
||
$border = New-Object System.Windows.Controls.Border
|
||
$border.CornerRadius = [System.Windows.CornerRadius]::new(6); $border.Padding = [System.Windows.Thickness]::new(9,8,9,8); $border.Margin = [System.Windows.Thickness]::new(0,0,0,4); $border.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#171A20')
|
||
$grid = New-Object System.Windows.Controls.Grid
|
||
$c1=New-Object System.Windows.Controls.ColumnDefinition; $c1.Width=[System.Windows.GridLength]::new(28); $grid.ColumnDefinitions.Add($c1)
|
||
$c2=New-Object System.Windows.Controls.ColumnDefinition; $c2.Width=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star); $grid.ColumnDefinitions.Add($c2)
|
||
$icon=New-Object System.Windows.Controls.TextBlock; $icon.Text='○'; $icon.Foreground='#6E7886'; $icon.FontSize=16; $icon.VerticalAlignment='Center'
|
||
$label=New-Object System.Windows.Controls.TextBlock; $label.Text=$step.title; $label.Foreground='#AAB3BF'; $label.VerticalAlignment='Center'; [System.Windows.Controls.Grid]::SetColumn($label,1)
|
||
$grid.Children.Add($icon)|Out-Null; $grid.Children.Add($label)|Out-Null; $border.Child=$grid; $StepsPanel.Children.Add($border)|Out-Null
|
||
$script:StepControls[$step.id]=@{Border=$border;Icon=$icon;Label=$label}
|
||
}
|
||
|
||
$script:Worker=$null; $script:Timer=$null; $script:Terminal=$false; $script:ClosingAllowed=$false; $script:LastStatusAt=Get-Date; $script:StartedAt=Get-Date
|
||
$script:StatusFile=''; $script:EventFile=''; $script:InputFile=''; $script:CancelFile=''; $script:SessionFile=Join-Path $SessionRoot "$TargetVersion.json"; $script:LogFile=''; $script:StdoutFile=''; $script:StderrFile=''; $script:EventOffset=0; $script:LastRequestId=''; $script:LastError=''
|
||
|
||
function Set-Badge([string]$state) {
|
||
switch ($state) {
|
||
'success' { $StatusText.Text='ERFOLGREICH'; $StatusBadge.Background='#173322'; $StatusBadge.BorderBrush='#2C7A4B'; $StatusText.Foreground='#8FE0AC' }
|
||
'failed' { $StatusText.Text='GESTOPPT'; $StatusBadge.Background='#351B20'; $StatusBadge.BorderBrush='#8B3B48'; $StatusText.Foreground='#FFB6BE' }
|
||
'input' { $StatusText.Text='EINGABE'; $StatusBadge.Background='#392E17'; $StatusBadge.BorderBrush='#8C6D27'; $StatusText.Foreground='#F4D47B' }
|
||
'waiting' { $StatusText.Text='WARTE'; $StatusBadge.Background='#1B2D3B'; $StatusBadge.BorderBrush='#35627D'; $StatusText.Foreground='#91CBE9' }
|
||
'cancelling' { $StatusText.Text='ABBRUCH'; $StatusBadge.Background='#392E17'; $StatusBadge.BorderBrush='#8C6D27'; $StatusText.Foreground='#F4D47B' }
|
||
default { $StatusText.Text='LÄUFT'; $StatusBadge.Background='#1C2D49'; $StatusBadge.BorderBrush='#36598A'; $StatusText.Foreground='#9FC0FF' }
|
||
}
|
||
}
|
||
|
||
function Update-Steps($status) {
|
||
if (-not $status.steps) { return }
|
||
foreach ($s in @($status.steps)) {
|
||
if (-not $script:StepControls.ContainsKey([string]$s.id)) { continue }
|
||
$c=$script:StepControls[[string]$s.id]
|
||
switch ([string]$s.state) {
|
||
'done' { $c.Icon.Text='●'; $c.Icon.Foreground='#62D28B'; $c.Label.Foreground='#EEF1F5'; $c.Border.Background='#18251E' }
|
||
'active' { $c.Icon.Text='●'; $c.Icon.Foreground='#6EA1FF'; $c.Label.Foreground='#FFFFFF'; $c.Label.FontWeight='SemiBold'; $c.Border.Background='#1A2435' }
|
||
'failed' { $c.Icon.Text='×'; $c.Icon.Foreground='#FF7A88'; $c.Label.Foreground='#FFD2D7'; $c.Border.Background='#2A191D' }
|
||
'skipped' { $c.Icon.Text='–'; $c.Icon.Foreground='#77808D'; $c.Label.Foreground='#77808D'; $c.Border.Background='#171A20' }
|
||
default { $c.Icon.Text='○'; $c.Icon.Foreground='#6E7886'; $c.Label.Foreground='#AAB3BF'; $c.Label.FontWeight='Normal'; $c.Border.Background='#171A20' }
|
||
}
|
||
}
|
||
}
|
||
|
||
function Write-InitialStatus {
|
||
$steps=@(
|
||
@{id='package';title='Paket prüfen';state='pending'},@{id='tools';title='System prüfen';state='pending'},@{id='github';title='GitHub verbinden';state='pending'},
|
||
@{id='production';title='Produktion vorbereiten';state='pending'},@{id='workspace';title='Arbeitskopie erstellen';state='pending'},@{id='tests';title='Vollständig testen';state='pending'},
|
||
@{id='pullrequest';title='Pull Request erstellen';state='pending'},@{id='merge';title='Sicher mergen';state='pending'},@{id='deploy';title='Coolify deployen';state='pending'},@{id='verify';title='Produktion bestätigen';state='pending'}
|
||
)
|
||
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=0;phase='Updater wird vorbereitet';detail='Die überwachte Update-Umgebung wird initialisiert.';state='running';error='';remediation='';updatedAt=(Get-Date).ToString('o');logFile=$script:LogFile;sessionFile=$script:SessionFile;steps=$steps;request=$null}|ConvertTo-Json -Depth 8
|
||
[IO.File]::WriteAllText($script:StatusFile,$payload,(New-Object Text.UTF8Encoding($false)))
|
||
}
|
||
|
||
function Get-WorkerExitDiagnostics {
|
||
$parts=New-Object Collections.Generic.List[string]
|
||
try {
|
||
$stderr=[string]$script:Worker.StandardError.ReadToEnd()
|
||
if($stderr){$stderr|Set-Content -LiteralPath $script:StderrFile -Encoding UTF8;$parts.Add("PowerShell-Fehlerkanal:`n$stderr")}
|
||
} catch {}
|
||
try {
|
||
$stdout=[string]$script:Worker.StandardOutput.ReadToEnd()
|
||
if($stdout){$stdout|Set-Content -LiteralPath $script:StdoutFile -Encoding UTF8;$parts.Add("PowerShell-Ausgabe:`n$stdout")}
|
||
} catch {}
|
||
if(Test-Path -LiteralPath $script:LogFile){
|
||
try{
|
||
$tail=@(Get-Content -LiteralPath $script:LogFile -Encoding UTF8 -Tail 35) -join [Environment]::NewLine
|
||
if($tail){$parts.Add("Letzte Protokollzeilen:`n$tail")}
|
||
}catch{}
|
||
}
|
||
if($parts.Count -eq 0){$parts.Add('Der Prozess lieferte keinen Fehlertext. Die Bootstrap-Dateien im Laufzeitordner wurden dennoch erhalten.')}
|
||
return ($parts -join ([Environment]::NewLine+[Environment]::NewLine))
|
||
}
|
||
|
||
function Start-Worker {
|
||
$stamp=Get-Date -Format 'yyyyMMdd-HHmmss-fff'; $run=Join-Path $RuntimeRoot $stamp; New-Item -ItemType Directory -Path $run -Force|Out-Null
|
||
$script:StatusFile=Join-Path $run 'status.json'; $script:EventFile=Join-Path $run 'events.ndjson'; $script:InputFile=Join-Path $run 'input.json'; $script:CancelFile=Join-Path $run 'cancel.flag'
|
||
$script:LogFile=Join-Path $run 'updater.log'; $script:StdoutFile=Join-Path $run 'worker.stdout.log'; $script:StderrFile=Join-Path $run 'worker.stderr.log'
|
||
$script:EventOffset=0; $script:Terminal=$false; $script:ClosingAllowed=$false; $script:StartedAt=Get-Date; $script:LastStatusAt=Get-Date; $script:LastRequestId=''; $script:LastError=''
|
||
$ErrorPanel.Visibility='Collapsed'; $InputPanel.Visibility='Collapsed'; $CopyErrorButton.Visibility='Collapsed'; $RetryButton.Visibility='Collapsed'; $CloseButton.IsEnabled=$false; $CancelButton.IsEnabled=$true; $OpenLogButton.IsEnabled=$true; $LogBox.Clear()
|
||
$LogPathText.Text=[IO.Path]::GetFileName($script:LogFile)
|
||
[IO.File]::WriteAllText($script:LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') [UI] Laufzeitordner angelegt: $run$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
|
||
$OpenRuntimeButton.IsEnabled=$true
|
||
foreach($entry in $script:StepControls.Values){$entry.Icon.Text='○';$entry.Icon.Foreground='#6E7886';$entry.Label.Foreground='#AAB3BF';$entry.Label.FontWeight='Normal';$entry.Border.Background='#171A20'}
|
||
Set-Badge 'running'; $PhaseText.Text='Updater wird vorbereitet'; $DetailText.Text='Die überwachte Update-Umgebung wird initialisiert.'; $ProgressBar.Value=0; $PercentText.Text='0 %'
|
||
Write-InitialStatus
|
||
|
||
$args=@('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"{0}"' -f $WorkerHostScript),'-CoreScript',('"{0}"' -f $WorkerScript),'-StatusFile',('"{0}"' -f $script:StatusFile),'-EventFile',('"{0}"' -f $script:EventFile),'-InputFile',('"{0}"' -f $script:InputFile),'-CancelFile',('"{0}"' -f $script:CancelFile),'-SessionFile',('"{0}"' -f $script:SessionFile),'-LogFile',('"{0}"' -f $script:LogFile))
|
||
if($SkipProductionDeploy){$args+='-SkipProductionDeploy'}
|
||
$psi=New-Object Diagnostics.ProcessStartInfo
|
||
$psi.FileName='powershell.exe'; $psi.Arguments=($args -join ' '); $psi.UseShellExecute=$false; $psi.CreateNoWindow=$true; $psi.WorkingDirectory=$PackageRoot
|
||
$psi.RedirectStandardOutput=$true; $psi.RedirectStandardError=$true
|
||
$script:Worker=New-Object Diagnostics.Process; $script:Worker.StartInfo=$psi
|
||
if(-not $script:Worker.Start()){throw 'Der überwachte Update-Prozess konnte nicht gestartet werden.'}
|
||
}
|
||
|
||
function Read-NewEvents {
|
||
if(-not (Test-Path -LiteralPath $script:EventFile)){return}
|
||
$stream=[IO.File]::Open($script:EventFile,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite)
|
||
try {
|
||
if($script:EventOffset -gt $stream.Length){$script:EventOffset=0}
|
||
$stream.Seek($script:EventOffset,[IO.SeekOrigin]::Begin)|Out-Null
|
||
$reader=New-Object IO.StreamReader($stream,[Text.Encoding]::UTF8,$true,4096,$true)
|
||
while(-not $reader.EndOfStream){
|
||
$line=$reader.ReadLine(); if(-not $line){continue}
|
||
try{$e=$line|ConvertFrom-Json; $display=('{0} {1,-7} {2}' -f ([DateTime]::Parse($e.at).ToString('HH:mm:ss')),[string]$e.level,[string]$e.message)}catch{$display=$line}
|
||
$LogBox.AppendText($display+[Environment]::NewLine); $LogBox.ScrollToEnd()
|
||
}
|
||
$script:EventOffset=$stream.Position; $reader.Dispose()
|
||
} finally {$stream.Dispose()}
|
||
}
|
||
|
||
function Apply-Status($s) {
|
||
$script:LastStatusAt=Get-Date
|
||
$PhaseText.Text=[string]$s.phase; $DetailText.Text=[string]$s.detail
|
||
$p=[Math]::Max(0,[Math]::Min(100,[int]$s.percent)); $ProgressBar.Value=$p; $PercentText.Text="$p %"
|
||
Set-Badge ([string]$s.state); Update-Steps $s
|
||
if($s.logFile){$script:LogFile=[string]$s.logFile;$LogPathText.Text=[IO.Path]::GetFileName($script:LogFile);$OpenLogButton.IsEnabled=$true}
|
||
if([string]$s.state -eq 'input' -and $s.request){
|
||
$requestId=[string]$s.request.id
|
||
if($requestId -ne $script:LastRequestId){$script:LastRequestId=$requestId;$SecretInput.Clear();$InputTitle.Text=[string]$s.request.title;$InputPrompt.Text=[string]$s.request.prompt;$InputPanel.Visibility='Visible';$SecretInput.Focus()}
|
||
} else {$InputPanel.Visibility='Collapsed'}
|
||
if([string]$s.state -in @('success','failed','cancelled')){
|
||
$script:Terminal=$true;$script:ClosingAllowed=$true;$CancelButton.IsEnabled=$false;$CloseButton.IsEnabled=$true
|
||
if([string]$s.state -eq 'failed'){
|
||
$script:LastError=[string]$s.error; $ErrorTitle.Text='Update sicher gestoppt';$ErrorText.Text=[string]$s.error;$RemediationText.Text=[string]$s.remediation;$ErrorPanel.Visibility='Visible';$CopyErrorButton.Visibility='Visible';$RetryButton.Visibility='Visible'
|
||
} elseif([string]$s.state -eq 'cancelled'){
|
||
$script:LastError='Das Update wurde durch den Benutzer abgebrochen.';$ErrorTitle.Text='Update abgebrochen';$ErrorText.Text=$script:LastError;$RemediationText.Text='Ein erneuter Start setzt den kontrollierten Ablauf fort.';$ErrorPanel.Visibility='Visible';$CopyErrorButton.Visibility='Visible';$RetryButton.Visibility='Visible'
|
||
}
|
||
}
|
||
}
|
||
|
||
$SubmitInputButton.Add_Click({
|
||
$value=$SecretInput.Password.Trim(); if(-not $value){[System.Windows.MessageBox]::Show('Bitte einen Wert eintragen.','Vendoo Updater',[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Warning)|Out-Null;return}
|
||
@{requestId=$script:LastRequestId;value=$value;submittedAt=(Get-Date).ToString('o')}|ConvertTo-Json|Set-Content -LiteralPath $script:InputFile -Encoding UTF8
|
||
$SecretInput.Clear();$InputPanel.Visibility='Collapsed';$DetailText.Text='Eingabe wurde sicher übernommen und wird geprüft.'
|
||
})
|
||
$CancelButton.Add_Click({if($script:Worker -and -not $script:Worker.HasExited){New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null;Set-Badge 'cancelling';$DetailText.Text='Der aktuelle Prozess wird kontrolliert beendet. Bitte kurz geöffnet lassen.';$CancelButton.IsEnabled=$false}})
|
||
$RetryButton.Add_Click({ try { Start-Worker } catch { $script:LastError=$_.Exception.Message; $ErrorText.Text=$script:LastError; $ErrorPanel.Visibility='Visible'; Set-Badge 'failed' } })
|
||
$CloseButton.Add_Click({$script:ClosingAllowed=$true;$window.Close()})
|
||
$OpenLogButton.Add_Click({if($script:LogFile -and (Test-Path -LiteralPath $script:LogFile)){Start-Process notepad.exe -ArgumentList ('"{0}"' -f $script:LogFile)}elseif($script:StatusFile){$folder=Split-Path -Parent $script:StatusFile;if(Test-Path $folder){Start-Process explorer.exe -ArgumentList ('"{0}"' -f $folder)}}})
|
||
$OpenRuntimeButton.Add_Click({if($script:StatusFile){$folder=Split-Path -Parent $script:StatusFile;if(Test-Path $folder){Start-Process explorer.exe -ArgumentList ('"{0}"' -f $folder)}}})
|
||
$CopyErrorButton.Add_Click({if($script:LastError){[System.Windows.Clipboard]::SetText($script:LastError+[Environment]::NewLine+[Environment]::NewLine+'Protokoll: '+$script:LogFile)}})
|
||
$window.Add_Closing({param($sender,$e) if(-not $script:ClosingAllowed){if($script:Worker -and -not $script:Worker.HasExited){$choice=[System.Windows.MessageBox]::Show('Das Update läuft noch. Soll es kontrolliert abgebrochen werden?','Vendoo Updater',[System.Windows.MessageBoxButton]::YesNo,[System.Windows.MessageBoxImage]::Warning);if($choice -eq [System.Windows.MessageBoxResult]::Yes){New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null;$e.Cancel=$true;$CancelButton.IsEnabled=$false;$DetailText.Text='Abbruch wird ausgeführt …'}else{$e.Cancel=$true}}else{$e.Cancel=$true}}})
|
||
|
||
$script:Timer=New-Object System.Windows.Threading.DispatcherTimer
|
||
$script:Timer.Interval=[TimeSpan]::FromMilliseconds(350)
|
||
$script:Timer.Add_Tick({
|
||
try {
|
||
Read-NewEvents
|
||
if(Test-Path -LiteralPath $script:StatusFile){try{$s=Get-Content -LiteralPath $script:StatusFile -Raw -Encoding UTF8|ConvertFrom-Json;Apply-Status $s}catch{}}
|
||
$elapsed=(Get-Date)-$script:StartedAt;$idle=[int]((Get-Date)-$script:LastStatusAt).TotalSeconds
|
||
$ActivityText.Text=('Laufzeit {0:00}:{1:00}' -f [int]$elapsed.TotalMinutes,$elapsed.Seconds);$HeartbeatText.Text=if($idle -lt 5){'Status aktuell'}else{"Letzte Meldung vor $idle Sek."}
|
||
if($script:Worker -and $script:Worker.HasExited -and -not $script:Terminal){
|
||
if(Test-Path -LiteralPath $script:StatusFile){
|
||
try{$final=Get-Content -LiteralPath $script:StatusFile -Raw -Encoding UTF8|ConvertFrom-Json;Apply-Status $final}catch{}
|
||
}
|
||
if(-not $script:Terminal){
|
||
$diagnostics=Get-WorkerExitDiagnostics
|
||
$script:Terminal=$true;$script:ClosingAllowed=$true;$CancelButton.IsEnabled=$false;$CloseButton.IsEnabled=$true;$RetryButton.Visibility='Visible';$CopyErrorButton.Visibility='Visible';Set-Badge 'failed'
|
||
$PhaseText.Text='Updater-Prozess unerwartet beendet';$DetailText.Text='Die vollständige Startdiagnose wurde aufgezeichnet. Das Fenster bleibt geöffnet.'
|
||
$script:LastError="Der überwachte Worker-Host wurde mit Exit-Code $($script:Worker.ExitCode) beendet.`n`n$diagnostics"
|
||
$ErrorText.Text=$script:LastError;$RemediationText.Text='Protokoll öffnen zeigt die vollständige Ausnahme mit Datei und Zeile. Danach kann derselbe sichere Lauf erneut gestartet werden.';$ErrorPanel.Visibility='Visible'
|
||
}
|
||
}
|
||
} catch {
|
||
$script:LastError=$_.Exception.Message
|
||
}
|
||
})
|
||
|
||
try { Start-Worker; $script:Timer.Start(); $window.ShowDialog()|Out-Null }
|
||
finally { try{$script:Timer.Stop()}catch{};if($script:Worker -and -not $script:Worker.HasExited){try{New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null}catch{}};try{$mutex.ReleaseMutex()}catch{};$mutex.Dispose() }
|