first commit

This commit is contained in:
dritter 2025-09-29 10:09:35 +02:00
commit 43494f38bb
32 changed files with 1810 additions and 0 deletions

52
.gitignore vendored Normal file
View file

@ -0,0 +1,52 @@
# Build-Output
bin/
obj/
# Visual Studio
*.user
*.suo
*.userosscache
*.sln.docstates
*.userprefs
# Rider / JetBrains
.idea/
*.iml
# VS Code
.vscode/
# Paket-Manager
packages/
*.nupkg
*.snupkg
# Logs & Temporäres
*.log
*.tlog
*.vspscc
*.vssscc
*.cache
*.tmp
# Visual Studio Arbeitsordner
.vs/
# Resharper
_ReSharper*/
*.[Rr]e[Ss]harper
# Test Ergebnisse
TestResults/
*.trx
# Publish Output
publish/
out/
dist/
# Sonstige
[Bb]uild/
project.lock.json
artifacts/
Generated Files/

22
InstallMonitor.sln Normal file
View file

@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35527.113 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstallMonitor", "InstallMonitor\InstallMonitor.csproj", "{45E112A5-43DB-436E-86E1-F112A73F0B30}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{45E112A5-43DB-436E-86E1-F112A73F0B30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45E112A5-43DB-436E-86E1-F112A73F0B30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45E112A5-43DB-436E-86E1-F112A73F0B30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45E112A5-43DB-436E-86E1-F112A73F0B30}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,66 @@
<Window x:Class="InstallMonitor.AddServerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:InstallMonitor"
xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"
mc:Ignorable="d"
Title="AddServerWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="70"/>
<RowDefinition Height="10*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="8,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="5*"/>
<RowDefinition Height="5*"/>
</Grid.RowDefinitions>
<TextBlock
VerticalAlignment="Bottom"
Grid.Row="0"
Text="Log Monitoring"
FontSize="18"
FontWeight="Bold"/>
<TextBlock
VerticalAlignment="Top"
Grid.Row="1"
Text="Server hinzufügen"
FontSize="16"
FontWeight="SemiBold"/>
</Grid>
<Grid Grid.Row="1" Background="#eaeaea">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="7*" />
<ColumnDefinition Width="220"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" >
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="10*"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Content="Server:" Margin="5,0,0,0"/>
<TextBox Grid.Row="1" Margin="10,0,0,10" x:Name="UserInput" TextWrapping="Wrap" AcceptsReturn="True"/>
</Grid>
<Grid Grid.Column="1">
<Button Width="100" Height="100" Click="Button_Click">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="8*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<fa:IconBlock Grid.Row="0" Icon="Add" Foreground="Green" FontSize="40" FontWeight="ExtraBold"/>
<TextBlock Grid.Row="1" Text="Add servers" FontSize="14"/>
</Grid>
</Button>
</Grid>
</Grid>
</Grid>
</Window>

View file

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace InstallMonitor
{
/// <summary>
/// Interaktionslogik für AddServerWindow.xaml
/// </summary>
public partial class AddServerWindow : Window
{
public event EventHandler? ServerAdd;
public AddServerWindow()
{
InitializeComponent();
}
private void RequestAddServer(string server)
{
ServerAdd?.Invoke(server, EventArgs.Empty);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var servers = UserInput.Text.Split("\n");
foreach (var server in servers)
{
var serverName = server.TrimEnd();
if (!string.IsNullOrWhiteSpace(serverName))
{
RequestAddServer(serverName);
}
}
Close();
}
}
}

17
InstallMonitor/App.xaml Normal file
View file

@ -0,0 +1,17 @@
<Application x:Class="InstallMonitor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"
xmlns:local="clr-namespace:InstallMonitor"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View file

@ -0,0 +1,33 @@
using System.Configuration;
using System.Data;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
namespace InstallMonitor
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
AppHost.ConfigureServices();
base.OnStartup(e);
}
}
public static class AppHost
{
public static IServiceProvider Services { get; private set; }
public static void ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<ConfigService>();
Services = services.BuildServiceProvider();
}
}
}

View file

@ -0,0 +1,21 @@
namespace InstallMonitor
{
public class AppSettings
{
public List<string> Servers { get; set; } = new();
public int MaxRetries { get; set; } = 3;
public int RetryTimeout { get; set; } = 20000;
public int InstallationProgressTimeout { get; set; } = 15;
public string LogFileDirectory { get; set; } = @"\\{ServerName}\C$\Program Files\Schleupen\log";
public string LogFileNamePattern { get; set; } = @"CSdeploy_2*.log";
public string GetResolvedLogFilePath(string serverName)
{
if (string.IsNullOrWhiteSpace(LogFileDirectory) || string.IsNullOrWhiteSpace(serverName))
return string.Empty;
return LogFileDirectory.Replace("{ServerName}", serverName);
}
}
}

View file

@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View file

@ -0,0 +1,71 @@
using System.IO;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
namespace InstallMonitor
{
public class ConfigService
{
private const string ConfigFileName = "appsettings.json";
public AppSettings Settings { get; private set; }
public ConfigService()
{
LoadOrCreateDefaultConfig();
}
private void LoadOrCreateDefaultConfig()
{
if (!File.Exists(ConfigFileName))
{
Settings = new AppSettings
{
Servers = new List<string> { "Server1", "Server2" }
};
Save();
}
else
{
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile(ConfigFileName, optional: false, reloadOnChange: false)
.Build();
Settings = config.Get<AppSettings>() ?? new AppSettings();
}
}
public void Save()
{
var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(Settings, options);
File.WriteAllText(ConfigFileName, json);
}
public bool ContainsServer(string name) => Settings.Servers.Any(s => string.Equals(s, name, StringComparison.OrdinalIgnoreCase));
public void AddServer(string name)
{
if (!ContainsServer(name))
{
Settings.Servers.Add(name);
Save();
}
}
public void RemoveServer(string name)
{
var serverToRemove = Settings.Servers
.FirstOrDefault(s => string.Equals(s, name, StringComparison.OrdinalIgnoreCase));
if (serverToRemove != null)
{
Settings.Servers.Remove(serverToRemove);
Save();
}
}
}
}

View file

@ -0,0 +1,41 @@
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace InstallMonitor
{
public class BooleanToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolVal)
{
return boolVal
? Brushes.Green
: Brushes.Red;
}
return Brushes.Red;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
public class InverseBooleanToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolVal)
{
return boolVal
? Brushes.Red
: Brushes.Green;
}
return Brushes.Green;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

View file

@ -0,0 +1,31 @@

using System.Globalization;
using System.Windows.Data;
using FontAwesome.Sharp;
namespace InstallMonitor
{
public class BooleanToIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool isOk = value is bool b && b;
return isOk ? IconChar.CheckCircle : IconChar.ExclamationCircle;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
public class InverseBooleanToIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool isOk = value is bool b && b;
return isOk ? IconChar.ExclamationCircle : IconChar.CheckCircle;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace InstallMonitor
{
public class BooleanToTextDecorationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolVal)
{
return boolVal
? TextDecorations.Strikethrough
: null;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
public class InverseBooleanToTextDecorationConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolVal)
{
return boolVal
? null
: TextDecorations.Strikethrough;
}
return TextDecorations.Strikethrough;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

View file

@ -0,0 +1,29 @@
using System.Globalization;
using System.Windows.Data;
using System.Windows;
namespace InstallMonitor
{
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return b ? Visibility.Visible : Visibility.Collapsed;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
public class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> (value is bool b && !b) ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

View file

@ -0,0 +1,23 @@
using System.Globalization;
using System.Windows.Data;
namespace InstallMonitor
{
public class DateTimeToLocalizedStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime dt)
{
string format = parameter as string ?? "f";
return dt.ToString(format, new CultureInfo("de-de"));
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<ApplicationIcon>schleupengrc_logo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="schleupengrc_logo.jpg" />
</ItemGroup>
<ItemGroup>
<Content Include="schleupengrc_logo.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FontAwesome.Sharp" Version="6.6.0" />
<PackageReference Include="MahApps.Metro" Version="2.4.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
</ItemGroup>
<ItemGroup>
<Resource Include="schleupengrc_logo.jpg" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace InstallMonitor
{
public record LogEntry : INotifyPropertyChanged
{
public string RawLine { get; set; }
public string? Phase { get; set; }
public string? Message { get; set; }
public bool IsStepCompletion { get; set; }
public bool IsError { get; set; } = false;
private bool _isIgnored = false;
public bool IsIgnored { get => _isIgnored; set { _isIgnored = value; OnPropertyChanged(); } }
public int CurrentStep { get; set; }
public int TotalSteps { get; set; }
public DateTime Timestamp { get; set; }
public TimeSpan Duration => DateTime.Now - Timestamp;
public event EventHandler? ServerCardIgnored;
public event EventHandler? PhaseGroupIgnored;
public void Ignore()
{
var handle = ServerCardIgnored;
if (handle != null)
handle.Invoke(this, EventArgs.Empty);
handle = PhaseGroupIgnored;
if (handle != null)
handle.Invoke(this, EventArgs.Empty);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View file

@ -0,0 +1,50 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace InstallMonitor
{
public static class LogParser
{
public static LogEntry ParseLogLine(string line)
{
var result = new LogEntry { RawLine = line };
var timestampMatch = Regex.Match(line, @"^(?<ts>\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2})");
if (timestampMatch.Success)
{
if (DateTime.TryParseExact(timestampMatch.Groups["ts"].Value, "dd.MM.yyyy HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var timestamp))
{
result.Timestamp = timestamp;
}
line = line.Substring(timestampMatch.Length);
}
var phaseMatch = Regex.Match(line, @"^\s+(DeployCS-[^:]+):$");
if (phaseMatch.Success)
{
result.Phase = phaseMatch.Groups[1].Value;
return result;
}
var stepMatch = Regex.Match(line, @"^\s+\[Schritt (\d+)/(\d+) beendet\.\]$");
if (stepMatch.Success)
{
result.IsStepCompletion = true;
result.CurrentStep = int.Parse(stepMatch.Groups[1].Value);
result.TotalSteps = int.Parse(stepMatch.Groups[2].Value);
result.Message = line;
return result;
}
var errorMatch = Regex.Match(line, @"^\s+Fehler\: (.+)$");
if (errorMatch.Success)
{
result.IsError = true;
}
result.Message = line;
return result;
}
}
}

View file

@ -0,0 +1,71 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace InstallMonitor
{
public class LogPhaseGroup : INotifyPropertyChanged
{
public string Phase { get; set; } = string.Empty;
public bool HasErrors => Entries.Any(e => e.IsError && !e.IsIgnored);
public bool HasIgnoredErrors => Entries.Any(e => e.IsError && e.IsIgnored);
public bool HasOnlyIgnoredErrors => HasIgnoredErrors && !HasErrors;
public bool HasEntries => Entries.Count > 0;
public int CurrentPhase { get; set; } = 0;
public int TotalPhases { get; set; } = 0;
public bool IsValidPhase => CurrentPhase > 0 && TotalPhases > 0;
public bool IsFinalPhase => IsValidPhase && CurrentPhase == TotalPhases;
public DateTime Timestamp { get; set; }
public TimeSpan Duration => DateTime.Now - Timestamp;
public TimeSpan? LastActivity
{
get
{
if (IsValidPhase)
{
var duration = Duration;
if (HasEntries)
{
return Entries.Last().Duration;
}
return duration;
}
return null;
}
}
public ObservableCollection<LogEntry> Entries { get; } = new();
public string StepLabel => IsValidPhase
? $"Schritt {CurrentPhase} von {TotalPhases}"
: "Ermittle Schritte ...";
public string Steps => IsValidPhase
? $"({CurrentPhase} / {TotalPhases})"
: string.Empty;
public int Progress => IsValidPhase
? (int)(((double)CurrentPhase / TotalPhases) * 100)
: 0;
public void AddEntry(LogEntry entry)
{
entry.PhaseGroupIgnored += OnLogEntryErrorIgnored;
Entries.Add(entry);
OnPropertyChanged(nameof(HasOnlyIgnoredErrors));
OnPropertyChanged(nameof(HasErrors));
}
private void OnLogEntryErrorIgnored(object? sender, EventArgs e)
{
OnPropertyChanged(nameof(HasOnlyIgnoredErrors));
OnPropertyChanged(nameof(HasErrors));
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

142
InstallMonitor/LogReader.cs Normal file
View file

@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InstallMonitor
{
public class LogReader
{
private CancellationTokenSource _cts;
private ConfigService _configService;
private long _lastPosition = 0;
private int _retryCount = 0;
public LogReader(ConfigService config, CancellationTokenSource cts)
{
_configService = config;
_cts = cts;
}
public string Directory => _configService.Settings.LogFileDirectory;
public string FilePattern => _configService.Settings.LogFileNamePattern;
public int MaxRetries => _configService.Settings.MaxRetries;
public int RetryTimeout => _configService.Settings.RetryTimeout;
public string LogFile { get; private set; } = string.Empty;
public event EventHandler? InternalError;
public event EventHandler? ReadLine;
public event EventHandler? StreamOpen;
public event EventHandler? AwaitLine;
public event EventHandler? TaskFailed;
private void FindLogFile()
{
var file = new DirectoryInfo(Directory)
.GetFiles(FilePattern)
.OrderByDescending(f => f.Name)
.FirstOrDefault();
if (file != null)
{
LogFile = file.FullName;
}
if (!File.Exists(LogFile))
throw new IOException("Datei nicht gefunden");
}
public async Task ReadLogAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
try
{
FindLogFile();
using var fs = new FileStream(LogFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(fs, System.Text.Encoding.UTF8, true);
StreamOpened();
if (_lastPosition == 0)
{
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
LineRead(line);
}
_lastPosition = fs.Position;
}
while (!_cts.IsCancellationRequested)
{
fs.Seek(_lastPosition, SeekOrigin.Begin);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
LineRead(line);
}
_lastPosition = fs.Position;
await Task.Delay(200, _cts.Token);
LineAwaited();
}
}
catch (OperationCanceledException ex) { }
catch (Exception ex) when (_retryCount < MaxRetries)
{
InternalErrorCaught($"Versuch: ({_retryCount + 1}/{MaxRetries}) - {ex.Message}");
_retryCount++;
await Task.Delay(RetryTimeout, _cts.Token);
}
catch (Exception ex)
{
TaskHasFailed($"Versuch: (no more retries) - {ex.Message}");
break;
}
}
}
public void StreamOpened()
{
_retryCount = 0;
var handle = StreamOpen;
if(handle != null)
handle.Invoke(null, EventArgs.Empty);
}
public void LineRead(string line)
{
var handle = ReadLine;
if (handle != null)
handle.Invoke(line, EventArgs.Empty);
}
public void LineAwaited()
{
var handle = AwaitLine;
if (handle != null)
handle.Invoke(null, EventArgs.Empty);
}
public void InternalErrorCaught(string message)
{
var handle = InternalError;
if (handle != null)
handle.Invoke(message, EventArgs.Empty);
}
public void TaskHasFailed(string message)
{
var handle = InternalError;
if (handle != null)
handle.Invoke(message, EventArgs.Empty);
}
}
}

View file

@ -0,0 +1,80 @@
<Window x:Class="InstallMonitor.LogTreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"
xmlns:local="clr-namespace:InstallMonitor"
mc:Ignorable="d"
Title="LogTreeView" Height="900" Width="1200">
<Window.Resources>
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<local:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
<local:BooleanToIconConverter x:Key="BooleanToIconConverter"/>
<local:InverseBooleanToIconConverter x:Key="InverseBooleanToIconConverter"/>
<local:BooleanToColorConverter x:Key="BooleanToColorConverter"/>
<local:InverseBooleanToColorConverter x:Key="InverseBooleanToColorConverter"/>
<local:BooleanToTextDecorationConverter x:Key="BooleanToTextDecorationConverter"/>
<local:InverseBooleanToTextDecorationConverter x:Key="InverseBooleanToTextDecorationConverter"/>
<local:DateTimeToLocalizedStringConverter x:Key="DateTimeConverter"/>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Entries}">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<fa:IconBlock
Icon="ScrewdriverWrench"
Visibility="{Binding HasOnlyIgnoredErrors, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="#1e3050"/>
<fa:IconBlock
Icon="{Binding HasErrors, Converter={StaticResource InverseBooleanToIconConverter}, FallbackValue=CheckCircle}"
Foreground="{Binding HasErrors, Converter={StaticResource InverseBooleanToColorConverter}, FallbackValue=Green}"
Visibility="{Binding HasOnlyIgnoredErrors, Converter={StaticResource InverseBoolToVisibilityConverter}}"/>
<TextBlock Text="{Binding Phase}" FontWeight="Bold" Grid.Column="1"/>
<TextBlock Text="{Binding Steps}" Margin="8,0,0,0" Foreground="Gray" Grid.Column="2"/>
</Grid>
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<StackPanel
Orientation="Horizontal" Margin="4,2">
<StackPanel.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem
Header="Fehler ignorieren"
Click="IgnoreError_Click"
CommandParameter="{Binding}"
Visibility="{Binding IsError, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</ContextMenu>
</StackPanel.ContextMenu>
<fa:IconBlock
Icon="ScrewdriverWrench"
Visibility="{Binding IsIgnored, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="#1e3050"/>
<fa:IconBlock
Icon="{Binding IsError, Converter={StaticResource InverseBooleanToIconConverter}, FallbackValue=CheckCircle}"
Foreground="{Binding IsError, Converter={StaticResource InverseBooleanToColorConverter}, FallbackValue=Green}"
Visibility="{Binding IsIgnored, Converter={StaticResource InverseBoolToVisibilityConverter}}"/>
<TextBlock
Text="{Binding Timestamp, Converter={StaticResource DateTimeConverter}, ConverterParameter='ddd, dd MMMM yyyy H:mm:ss'}"
TextDecorations="{Binding IsIgnored, Converter={StaticResource BooleanToTextDecorationConverter}}"
Foreground="Gray"
Margin="8,0,8,0"/>
<TextBlock Text="{Binding Message}" TextDecorations="{Binding IsIgnored, Converter={StaticResource BooleanToTextDecorationConverter}}"/>
</StackPanel>
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using WinForms = System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace InstallMonitor
{
/// <summary>
/// Interaktionslogik für LogTreeView.xaml
/// </summary>
public partial class LogTreeView : Window
{
public LogTreeView(ObservableCollection<LogPhaseGroup> phaseGroups)
{
InitializeComponent();
DataContext = phaseGroups;
}
private void IgnoreError_Click(object sender, RoutedEventArgs e)
{
if (sender is MenuItem menuItem && menuItem.CommandParameter is LogEntry entry)
{
WinForms.DialogResult dr = WinForms.MessageBox.Show("Do you want to ignore this error?", "Are you sure?", WinForms.MessageBoxButtons.YesNoCancel, WinForms.MessageBoxIcon.Warning);
if (dr == WinForms.DialogResult.Yes)
{
entry.Ignore();
}
}
}
}
}

View file

@ -0,0 +1,76 @@
<Window x:Class="InstallMonitor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:InstallMonitor"
xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"
Title="Server Monitor"
Width="1200" Height="920"
Background="White"
Closing="Window_Closing">
<Grid>
<!-- Layout in 2 Zeilen: Hauptbereich (90%) + Footer (10%) -->
<Grid.RowDefinitions>
<RowDefinition Height="75" />
<RowDefinition Height="8.75*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Server hinzufügen" Click="MenuItem_AddServers"/>
</ContextMenu>
</Grid.ContextMenu>
<Border Grid.Row="0" Background="#f3f3f3" Padding="25,10">
<Grid>
<Grid Width="60" HorizontalAlignment="Left">
<Image Source="/schleupengrc_logo.jpg"/>
</Grid>
<Grid Margin="70,0,0,0" VerticalAlignment="Center">
<TextBlock Text="Schleupen.CS" FontSize="18" FontWeight="Bold"/>
<TextBlock Text="Log-Monitoring" FontSize="16" FontWeight="SemiBold" Margin="0,25,0,0"/>
</Grid>
<Grid HorizontalAlignment="Right">
<Button
x:Name="StartButton"
Click="StartMonitor_Click"
Width="50"
Height="50"
FontSize="25"
Content="{fa:Icon Play, Foreground=Green}" />
<Button
x:Name="StopButton"
Click="StopMonitor_Click"
Visibility="Hidden"
Width="50"
Height="50"
FontSize="25"
Content="{fa:Icon Stop, Foreground=Red}" />
</Grid>
</Grid>
</Border>
<!-- Hauptcontainer mit ScrollViewer -->
<Border Grid.Row="1" Background="#eaeaea">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<WrapPanel Margin="20"
HorizontalAlignment="Stretch"
ItemWidth="360"
ItemHeight="160"
x:Name="ServerCards"
Orientation="Horizontal">
</WrapPanel>
</ScrollViewer>
</Border>
<!-- Footer mit Logo/Text -->
<Border Grid.Row="2" Background="#f3f3f3" Padding="10">
<TextBlock Text="© 2025 by DURI" FontSize="16" FontWeight="SemiBold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,117 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Extensions.DependencyInjection;
namespace InstallMonitor
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ConfigService ConfigService = AppHost.Services.GetRequiredService<ConfigService>();
public MainWindow()
{
InitializeComponent();
foreach (var serverName in ConfigService.Settings.Servers)
{
AddServerCard(serverName);
}
}
private async void Window_Closing(object sender, CancelEventArgs e)
{
await StopMonitor();
Application.Current.Shutdown();
}
public void StartMonitor()
{
StopButton.Visibility = Visibility.Visible;
StartButton.Visibility = Visibility.Hidden;
foreach (ServerCard server in ServerCards.Children)
{
server.ViewModel.StartMonitor();
}
}
public async Task StopMonitor()
{
StopButton.Visibility = Visibility.Hidden;
StartButton.Visibility = Visibility.Visible;
var stopTasks = new List<Task>();
foreach (ServerCard server in ServerCards.Children)
{
if (server.ViewModel.IsMonitoring)
{
var task = server.ViewModel.StopMonitor();
stopTasks.Add(task);
}
}
await Task.WhenAll(stopTasks);
}
public void MenuItem_AddServers(object sender, RoutedEventArgs e)
{
var viewer = new AddServerWindow();
viewer.Title = "Server hinzufügen";
viewer.ServerAdd += OnServerAdd;
viewer.Show();
}
private void OnServerDelete(object? sender, EventArgs e)
{
if (sender is ServerCardViewModel server)
{
var serverCard = ServerCards.Children.OfType<ServerCard>().First(s => s.DataContext.Equals(server));
ServerCards.Children.Remove(serverCard);
ConfigService.RemoveServer(server.ServerName);
}
}
private void OnServerAdd(object? sender, EventArgs e)
{
if (sender is string serverName)
{
if (!ConfigService.ContainsServer(serverName))
{
AddServerCard(serverName);
ConfigService.AddServer(serverName);
}
}
}
private void AddServerCard(string serverName)
{
var server = new ServerCard
{
DataContext = new ServerCardViewModel(ConfigService)
{
ServerName = serverName,
}
};
server.ViewModel.Deleted += OnServerDelete;
ServerCards.Children.Add(server);
}
public void StartMonitor_Click(object sender, RoutedEventArgs e) => StartMonitor();
public async void StopMonitor_Click(object sender, RoutedEventArgs e) => await StopMonitor();
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net8.0-windows\publish\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,125 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fa="http://schemas.awesome.incremented/wpf/xaml/fontawesome.sharp"
xmlns:local="clr-namespace:InstallMonitor"
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" x:Class="InstallMonitor.ServerCard"
Width="340" Height="150" Background="White">
<UserControl.Resources>
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<local:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
<local:BooleanToColorConverter x:Key="BooleanToColorConverter"/>
<local:InverseBooleanToColorConverter x:Key="InverseBooleanToColorConverter"/>
<local:BooleanToIconConverter x:Key="BooleanToIconConverter"/>
<local:InverseBooleanToIconConverter x:Key="InverseBooleanToIconConverter"/>
<Style TargetType="{x:Type ProgressBar}" BasedOn="{StaticResource {x:Type ProgressBar}}"/>
</UserControl.Resources>
<Grid>
<Border Background="White" CornerRadius="4" Width="340" Height="150" MinWidth="100" MinHeight="100"
BorderBrush="#ddd" BorderThickness="1">
<Border.Effect>
<DropShadowEffect BlurRadius="8" ShadowDepth="2" Opacity="0.2" />
</Border.Effect>
<Grid Margin="12" Background="White">
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Click="MenuItem_ShowLogDetails" Header="Details anzeigen"/>
<Separator />
<MenuItem Click="MenuItem_StartMonitor" Header="Monitoring starten"/>
<MenuItem Click="MenuItem_StopMonitor" Header="Monitoring stoppen"/>
<MenuItem Click="MenuItem_RequestDelete" Header="Server löschen"/>
<Separator/>
<MenuItem Header="Server neustarten"/>
</ContextMenu>
</Grid.ContextMenu>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Background="White" >
<Grid HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding ServerName, FallbackValue=ServerName}" FontSize="20" FontWeight="Bold" />
<fa:IconBlock
Visibility="{Binding IsInstallationProgressTimeout, Converter={StaticResource BooleanToVisibilityConverter}, FallbackValue=Visible}"
FontSize="20"
Foreground="Gray"
Icon="HourglassHalf"
Margin="10,0,0,0"
Grid.Column="1"/>
</Grid>
<Grid HorizontalAlignment="Right">
<Grid Height="30"
Visibility="{Binding IsMonitoring, Converter={StaticResource BooleanToVisibilityConverter}}"
HorizontalAlignment="Center">
<Grid Visibility="{Binding IsInternalError, Converter={StaticResource InverseBoolToVisibilityConverter}, FallbackValue=Hidden}">
<Grid
Visibility="{Binding IsInitializing, Converter={StaticResource InverseBoolToVisibilityConverter}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="35"/>
<ColumnDefinition Width="35"/>
</Grid.ColumnDefinitions>
<TextBlock
Text="{Binding ErrorsCount, FallbackValue=23}"
HorizontalAlignment="Center"
Visibility="{Binding HasErrors, Converter={StaticResource BooleanToVisibilityConverter}}"
FontSize="20" Grid.Column="0"/>
<fa:IconBlock
FontSize="30"
Icon="{Binding HasErrors, Converter={StaticResource InverseBooleanToIconConverter}, FallbackValue=CheckCircle}"
Foreground="{Binding HasErrors, Converter={StaticResource InverseBooleanToColorConverter}, FallbackValue=Green}"
Grid.Column="1"/>
</Grid>
<Grid Visibility="{Binding IsInitializing, Converter={StaticResource BooleanToVisibilityConverter}}">
<mah:ProgressRing IsActive="{Binding IsInitializing}" Width="30" Height="30" />
</Grid>
</Grid>
<Grid Visibility="{Binding IsInternalError, Converter={StaticResource BooleanToVisibilityConverter}, FallbackValue=Visible}">
<fa:IconBlock
FontSize="30"
Foreground="#ffc107"
Icon="TriangleExclamation"/>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid Grid.Row="1">
<TextBlock FontSize="12"
FontWeight="SemiBold"
HorizontalAlignment="Center"
TextWrapping="Wrap"
Text="{Binding PhaseLabelText, FallbackValue=CurrentPhase}" VerticalAlignment="Top" Height="36" Width="314" Margin="0,43,0,0"/>
<TextBlock
FontSize="12"
HorizontalAlignment="Center"
TextWrapping="Wrap"
fa:Awesome.Inline="{Binding StepLabelText, FallbackValue=CurrentStepLabel}"
VerticalAlignment="Top" Height="15" Width="314" Margin="0,27,0,0"/>
</Grid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ProgressBar Value="{Binding CurrentProgress, FallbackValue=30, Mode=OneWay}"
Style="{x:Null}"
Height="12"
VerticalAlignment="Center" Grid.ColumnSpan="2"
Foreground="{Binding HasProblems, Converter={StaticResource InverseBooleanToColorConverter}, FallbackValue=Green}"
Visibility="{Binding IsMonitoring, Converter={StaticResource BooleanToVisibilityConverter}}"
BorderThickness="0"
IsIndeterminate="{Binding IsProgressBarIsIndeterminate}"/>
</Grid>
</Grid>
</Border>
</Grid>
</UserControl>

View file

@ -0,0 +1,59 @@
using System.Windows;
using System.Windows.Controls;
using WinForm = System.Windows.Forms;
namespace InstallMonitor
{
public partial class ServerCard : UserControl
{
public ServerCard()
{
InitializeComponent();
}
public ServerCardViewModel ViewModel
{
get => (ServerCardViewModel)DataContext;
set => DataContext = value;
}
private void MenuItem_ShowLogDetails(object sender, RoutedEventArgs e)
{
if (DataContext is ServerCardViewModel vm)
{
var viewer = new LogTreeView(vm.LogPhaseGroups);
viewer.Title = $"Details - {vm.ServerName} - Install Monitor";
viewer.Show();
}
}
private void MenuItem_StartMonitor(object sender, RoutedEventArgs e)
{
if (DataContext is ServerCardViewModel vm)
{
vm.StartMonitor();
}
}
private async void MenuItem_StopMonitor(object sender, RoutedEventArgs e)
{
if (DataContext is ServerCardViewModel vm)
{
await vm.StopMonitor();
}
}
private void MenuItem_RequestDelete(object sender, RoutedEventArgs e)
{
if (DataContext is ServerCardViewModel vm)
{
WinForm.DialogResult dr = WinForm.MessageBox.Show($"Do you want to remove {vm.ServerName}?", "Are you sure?", WinForm.MessageBoxButtons.YesNoCancel, WinForm.MessageBoxIcon.Warning);
if (dr == WinForm.DialogResult.Yes)
{
vm.RequestDelete();
}
}
}
}
}

View file

@ -0,0 +1,263 @@

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
namespace InstallMonitor
{
public enum ServerStatus
{
Stopped,
Initializing,
InternalError,
Running,
RunningWithErrors,
InstallationProgressTimeout,
CompletedWithErrors,
Completed,
Failed
}
public class ServerCardViewModel : INotifyPropertyChanged
{
public ServerCardViewModel(ConfigService config)
{
Config = config;
}
public string ServerName { get; set; } = string.Empty;
public ObservableCollection<LogEntry> ErrorCollection { get; set; } = new();
public ObservableCollection<LogPhaseGroup> LogPhaseGroups { get; set; } = new()
{
new LogPhaseGroup
{
Phase = "Init Phase"
}
};
public CancellationTokenSource Cts = new CancellationTokenSource();
public Task? MonitoringTask;
public ConfigService Config;
public event EventHandler? Deleted;
public string InternalError
{
get => _internalError;
set { _internalError = value; OnPropertyChanged(); }
}
public ServerStatus Status
{
get => _status;
set
{
if (_status != value)
{
_status = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsProgressBarIsIndeterminate));
OnPropertyChanged(nameof(HasErrors));
OnPropertyChanged(nameof(HasProblems));
OnPropertyChanged(nameof(ErrorsCount));
OnPropertyChanged(nameof(IsMonitoring));
OnPropertyChanged(nameof(IsInitializing));
OnPropertyChanged(nameof(IsInternalError));
OnPropertyChanged(nameof(IsInstallationProgressTimeout));
OnPropertyChanged(nameof(IsStopped));
OnPropertyChanged(nameof(StepLabelText));
OnPropertyChanged(nameof(PhaseLabelText));
if (HasMonitored)
{
Cts.Cancel();
}
}
}
}
public bool IsProgressBarIsIndeterminate => !HasStepInfo || IsInternalError;
public bool IsStopped => Status == ServerStatus.Stopped;
public bool IsInitializing => Status == ServerStatus.Initializing;
public bool IsInternalError => Status == ServerStatus.InternalError;
public bool IsRunning => Status == ServerStatus.Running;
public bool IsRunningWithErrors => Status == ServerStatus.RunningWithErrors;
public bool IsInstallationProgressTimeout => Status == ServerStatus.InstallationProgressTimeout;
public bool IsCompletedWithErrors => Status == ServerStatus.CompletedWithErrors;
public bool IsCompleted => Status == ServerStatus.Completed;
public bool IsFailed => Status == ServerStatus.Failed;
public bool IsMonitoring => !IsStopped && !IsCompleted && !IsCompletedWithErrors && !IsFailed;
public bool HasMonitored => IsCompleted || IsCompletedWithErrors || IsFailed;
public bool HasStepInfo => LogPhaseGroups.Last().TotalPhases > 0;
public bool HasPhase => LogPhaseGroups.Count > 1;
public bool HasProblems => HasErrors || IsInternalError;
public bool HasErrors => ErrorCollection.Count > 0;
public int ErrorsCount => ErrorCollection.Count;
public LogPhaseGroup CurrentPhaseGroup => LogPhaseGroups.Last();
public string CurrentPhase => CurrentPhaseGroup.Phase;
public int CurrentProgress => CurrentPhaseGroup.Progress;
public string CurrentStepLabel => CurrentPhaseGroup.StepLabel;
public LogEntry CurrentLogEntry => CurrentPhaseGroup.Entries.Last();
public void CheckInstallationProgressTimeout()
{
TimeSpan? lastActivity = CurrentPhaseGroup.LastActivity;
if (lastActivity != null)
{
if (lastActivity.Value.TotalMinutes > Config.Settings.InstallationProgressTimeout)
{
Status = ServerStatus.InstallationProgressTimeout;
}
}
}
public void OnAwaitLine(object? sender, EventArgs e)
{
this.CheckInstallationProgressTimeout();
}
public void OnStreamOpen(object? sender, EventArgs e)
{
this.ResetInternalError();
}
public void OnLogReadLine(object? sender, EventArgs e)
{
if (sender is string line)
{
this.AddLog(line);
}
}
public void OnTaskFailed(object? sender, EventArgs e)
{
if (sender is string message)
{
this.SetInternalError(message);
Status = ServerStatus.Failed;
}
}
public void OnInternalError(object? sender, EventArgs e)
{
if (sender is string message)
{
this.SetInternalError(message);
}
}
public void OnIgnoreError(object? sender, EventArgs e)
{
if (sender is LogEntry error)
{
if (ErrorCollection.Contains(error))
{
ErrorCollection.Remove(error);
error.IsIgnored = true;
if (!HasErrors)
{
if (IsMonitoring)
Status = ServerStatus.Running;
else Status = ServerStatus.Completed;
}
}
}
}
public string StepLabelText
{
get
{
if (IsStopped)
return "Monitoring stopped";
if (IsInternalError)
return "Internal Error";
if (IsInitializing && !HasStepInfo)
return "Initializing steps ...";
if (IsRunning)
return CurrentStepLabel;
if (IsRunningWithErrors && HasStepInfo)
return CurrentStepLabel;
if (IsInstallationProgressTimeout)
return $"Last activity {(int)CurrentPhaseGroup.LastActivity!.Value.TotalMinutes} minutes ago";
if (IsCompleted)
return ":Check: Completed";
if (IsCompletedWithErrors)
return $":TriangleExclamation: Completed with {ErrorsCount} error(s).";
if (IsFailed)
return ":Xmark: Task failed";
return string.Empty;
}
}
public string PhaseLabelText
{
get
{
if (IsStopped)
return string.Empty;
if (IsInternalError)
return InternalError;
if (IsInitializing && !HasPhase)
return string.Empty;
if (IsInitializing && HasPhase)
return CurrentPhase;
if (IsRunning)
return CurrentPhase;
if (IsRunningWithErrors && HasPhase)
return CurrentPhase;
return string.Empty;
}
}
public void RefreshProgress()
{
OnPropertyChanged(nameof(StepLabelText));
OnPropertyChanged(nameof(PhaseLabelText));
OnPropertyChanged(nameof(CurrentProgress));
}
public async void RequestDelete()
{
await this.StopMonitor();
Deleted?.Invoke(this, EventArgs.Empty);
}
public event PropertyChangedEventHandler? PropertyChanged;
private ServerStatus _status = ServerStatus.Stopped;
private string _internalError = string.Empty;
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View file

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace InstallMonitor
{
public static class ServerCardViewModelInternalErrorExtensions
{
public static void ResetInternalError(this ServerCardViewModel vm)
{
vm.InternalError = string.Empty;
vm.Status = ServerStatus.Initializing;
}
public static void SetInternalError(this ServerCardViewModel vm, string message)
{
vm.InternalError = message;
vm.Status = ServerStatus.InternalError;
vm.RefreshProgress();
}
}
}

View file

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace InstallMonitor
{
public static class ServerCardViewModelLogExtensions
{
public static LogReader GetLogReader(this ServerCardViewModel vm)
{
var logReader = new LogReader(vm.Config, vm.Cts);
logReader.InternalError += vm.OnInternalError;
logReader.AwaitLine += vm.OnAwaitLine;
logReader.ReadLine += vm.OnLogReadLine;
logReader.StreamOpen += vm.OnStreamOpen;
logReader.TaskFailed += vm.OnTaskFailed;
return logReader;
}
public static void AddLog(this ServerCardViewModel vm, string logLine)
{
var entry = LogParser.ParseLogLine(logLine);
entry.ServerCardIgnored += vm.OnIgnoreError;
var currentPhase = vm.LogPhaseGroups.Last();
if (entry.Phase != null)
{
var nextPhase = new LogPhaseGroup { Phase = entry.Phase };
nextPhase.Timestamp = entry.Timestamp;
if (currentPhase.CurrentPhase > 0 &&
currentPhase.TotalPhases > 0)
{
nextPhase.CurrentPhase = currentPhase.CurrentPhase + 1;
nextPhase.TotalPhases = currentPhase.TotalPhases;
}
vm.AddLogPhase(nextPhase);
return;
}
if (entry.IsStepCompletion)
{
currentPhase.CurrentPhase = entry.CurrentStep;
currentPhase.TotalPhases = entry.TotalSteps;
}
if (string.IsNullOrWhiteSpace(entry.Message))
return;
vm.AddLogPhaseEntry(currentPhase, entry);
}
public static void AddLogPhase(this ServerCardViewModel vm, LogPhaseGroup logPhase)
{
if (logPhase.CurrentPhase > 0 &&
logPhase.TotalPhases > 0 &&
vm.IsInitializing)
vm.Status = ServerStatus.Running;
App.Current.Dispatcher.Invoke(() => vm.LogPhaseGroups.Add(logPhase));
vm.RefreshProgress();
}
public static void AddLogPhaseEntry(this ServerCardViewModel vm, LogPhaseGroup currentPhase, LogEntry entry)
{
if (entry.IsError)
{
vm.Status = ServerStatus.RunningWithErrors;
vm.ErrorCollection.Add(entry);
}
if (vm.IsInstallationProgressTimeout)
{
if (vm.HasErrors)
vm.Status = ServerStatus.RunningWithErrors;
else vm.Status = ServerStatus.Running;
}
if (entry.IsStepCompletion && currentPhase.IsFinalPhase)
{
if (vm.HasErrors)
vm.Status = ServerStatus.CompletedWithErrors;
else
vm.Status = ServerStatus.Completed;
return;
}
if (entry.IsStepCompletion)
return;
App.Current.Dispatcher.Invoke(() => currentPhase.AddEntry(entry));
}
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics.X86;
using System.Text;
using System.Threading.Tasks;
using ControlzEx.Standard;
namespace InstallMonitor
{
public static class ServerCardViewModelMonitorExtensions
{
public static void StartMonitor(this ServerCardViewModel vm)
{
if (vm.IsMonitoring)
return;
vm.ClearMonitor();
var logReader = vm.GetLogReader();
vm.MonitoringTask = Task.Run(async () => await logReader.ReadLogAsync());
}
public static async Task StopMonitor(this ServerCardViewModel vm)
{
vm.Cts.Cancel();
try
{
if (vm.MonitoringTask != null)
await vm.MonitoringTask;
}
catch (OperationCanceledException) { }
vm.Status = ServerStatus.Stopped;
}
public static void ClearMonitor(this ServerCardViewModel vm)
{
vm.ErrorCollection = new();
vm.LogPhaseGroups = new()
{
new LogPhaseGroup
{
Phase = "Init Phase"
}
};
vm.ResetInternalError();
vm.Cts = new CancellationTokenSource();
vm.Status = ServerStatus.Initializing;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB