From 43494f38bb96d5e48c750346fd7590e91056eb82 Mon Sep 17 00:00:00 2001 From: dritter Date: Mon, 29 Sep 2025 10:09:35 +0200 Subject: [PATCH] first commit --- .gitignore | 52 ++++ InstallMonitor.sln | 22 ++ InstallMonitor/AddServerWindow.xaml | 66 +++++ InstallMonitor/AddServerWindow.xaml.cs | 50 ++++ InstallMonitor/App.xaml | 17 ++ InstallMonitor/App.xaml.cs | 33 +++ InstallMonitor/AppSettings.cs | 21 ++ InstallMonitor/AssemblyInfo.cs | 10 + InstallMonitor/ConfigService.cs | 71 +++++ .../Converters/BooleanToColorConverter.cs | 41 +++ .../Converters/BooleanToIconConverter.cs | 31 +++ .../BooleanToTextDecorationConverter.cs | 45 +++ .../BooleanToVisibilityConverter.cs | 29 ++ .../DateTimeToLocalizedStringConverter.cs | 23 ++ InstallMonitor/InstallMonitor.csproj | 32 +++ InstallMonitor/LogEntry.cs | 43 +++ InstallMonitor/LogParser.cs | 50 ++++ InstallMonitor/LogPhaseGroup.cs | 71 +++++ InstallMonitor/LogReader.cs | 142 ++++++++++ InstallMonitor/LogTreeView.xaml | 80 ++++++ InstallMonitor/LogTreeView.xaml.cs | 43 +++ InstallMonitor/MainWindow.xaml | 76 +++++ InstallMonitor/MainWindow.xaml.cs | 117 ++++++++ .../PublishProfiles/FolderProfile.pubxml | 13 + InstallMonitor/ServerCard.xaml | 125 +++++++++ InstallMonitor/ServerCard.xaml.cs | 59 ++++ InstallMonitor/ServerCardViewModel.cs | 263 ++++++++++++++++++ ...verCardViewModelInternalErrorExtensions.cs | 24 ++ .../ServerCardViewModelLogExtensions.cs | 103 +++++++ .../ServerCardViewModelMonitorExtensions.cs | 58 ++++ InstallMonitor/schleupengrc_logo.ico | Bin 0 -> 165662 bytes InstallMonitor/schleupengrc_logo.jpg | Bin 0 -> 5518 bytes 32 files changed, 1810 insertions(+) create mode 100644 .gitignore create mode 100644 InstallMonitor.sln create mode 100644 InstallMonitor/AddServerWindow.xaml create mode 100644 InstallMonitor/AddServerWindow.xaml.cs create mode 100644 InstallMonitor/App.xaml create mode 100644 InstallMonitor/App.xaml.cs create mode 100644 InstallMonitor/AppSettings.cs create mode 100644 InstallMonitor/AssemblyInfo.cs create mode 100644 InstallMonitor/ConfigService.cs create mode 100644 InstallMonitor/Converters/BooleanToColorConverter.cs create mode 100644 InstallMonitor/Converters/BooleanToIconConverter.cs create mode 100644 InstallMonitor/Converters/BooleanToTextDecorationConverter.cs create mode 100644 InstallMonitor/Converters/BooleanToVisibilityConverter.cs create mode 100644 InstallMonitor/Converters/DateTimeToLocalizedStringConverter.cs create mode 100644 InstallMonitor/InstallMonitor.csproj create mode 100644 InstallMonitor/LogEntry.cs create mode 100644 InstallMonitor/LogParser.cs create mode 100644 InstallMonitor/LogPhaseGroup.cs create mode 100644 InstallMonitor/LogReader.cs create mode 100644 InstallMonitor/LogTreeView.xaml create mode 100644 InstallMonitor/LogTreeView.xaml.cs create mode 100644 InstallMonitor/MainWindow.xaml create mode 100644 InstallMonitor/MainWindow.xaml.cs create mode 100644 InstallMonitor/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 InstallMonitor/ServerCard.xaml create mode 100644 InstallMonitor/ServerCard.xaml.cs create mode 100644 InstallMonitor/ServerCardViewModel.cs create mode 100644 InstallMonitor/ServerCardViewModelInternalErrorExtensions.cs create mode 100644 InstallMonitor/ServerCardViewModelLogExtensions.cs create mode 100644 InstallMonitor/ServerCardViewModelMonitorExtensions.cs create mode 100644 InstallMonitor/schleupengrc_logo.ico create mode 100644 InstallMonitor/schleupengrc_logo.jpg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d24b1e0 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/InstallMonitor.sln b/InstallMonitor.sln new file mode 100644 index 0000000..2879b4b --- /dev/null +++ b/InstallMonitor.sln @@ -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 diff --git a/InstallMonitor/AddServerWindow.xaml b/InstallMonitor/AddServerWindow.xaml new file mode 100644 index 0000000..3a2870f --- /dev/null +++ b/InstallMonitor/AddServerWindow.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InstallMonitor/AddServerWindow.xaml.cs b/InstallMonitor/AddServerWindow.xaml.cs new file mode 100644 index 0000000..4744cb9 --- /dev/null +++ b/InstallMonitor/AddServerWindow.xaml.cs @@ -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 +{ + /// + /// Interaktionslogik für AddServerWindow.xaml + /// + 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(); + } + } +} diff --git a/InstallMonitor/App.xaml b/InstallMonitor/App.xaml new file mode 100644 index 0000000..3736d0b --- /dev/null +++ b/InstallMonitor/App.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/InstallMonitor/App.xaml.cs b/InstallMonitor/App.xaml.cs new file mode 100644 index 0000000..3438f58 --- /dev/null +++ b/InstallMonitor/App.xaml.cs @@ -0,0 +1,33 @@ +using System.Configuration; +using System.Data; +using System.Windows; +using Microsoft.Extensions.DependencyInjection; + +namespace InstallMonitor +{ + /// + /// Interaction logic for App.xaml + /// + 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(); + + Services = services.BuildServiceProvider(); + } + } +} diff --git a/InstallMonitor/AppSettings.cs b/InstallMonitor/AppSettings.cs new file mode 100644 index 0000000..71ff4de --- /dev/null +++ b/InstallMonitor/AppSettings.cs @@ -0,0 +1,21 @@ +namespace InstallMonitor +{ + public class AppSettings + { + public List 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); + } + } + +} diff --git a/InstallMonitor/AssemblyInfo.cs b/InstallMonitor/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/InstallMonitor/AssemblyInfo.cs @@ -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) +)] diff --git a/InstallMonitor/ConfigService.cs b/InstallMonitor/ConfigService.cs new file mode 100644 index 0000000..2813c7a --- /dev/null +++ b/InstallMonitor/ConfigService.cs @@ -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 { "Server1", "Server2" } + }; + + Save(); + } + else + { + var config = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile(ConfigFileName, optional: false, reloadOnChange: false) + .Build(); + + Settings = config.Get() ?? 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(); + } + } + + } +} diff --git a/InstallMonitor/Converters/BooleanToColorConverter.cs b/InstallMonitor/Converters/BooleanToColorConverter.cs new file mode 100644 index 0000000..62bf2e2 --- /dev/null +++ b/InstallMonitor/Converters/BooleanToColorConverter.cs @@ -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(); + } + +} diff --git a/InstallMonitor/Converters/BooleanToIconConverter.cs b/InstallMonitor/Converters/BooleanToIconConverter.cs new file mode 100644 index 0000000..ab5bb0c --- /dev/null +++ b/InstallMonitor/Converters/BooleanToIconConverter.cs @@ -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(); + } +} diff --git a/InstallMonitor/Converters/BooleanToTextDecorationConverter.cs b/InstallMonitor/Converters/BooleanToTextDecorationConverter.cs new file mode 100644 index 0000000..2f0583c --- /dev/null +++ b/InstallMonitor/Converters/BooleanToTextDecorationConverter.cs @@ -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(); + } +} diff --git a/InstallMonitor/Converters/BooleanToVisibilityConverter.cs b/InstallMonitor/Converters/BooleanToVisibilityConverter.cs new file mode 100644 index 0000000..f0254df --- /dev/null +++ b/InstallMonitor/Converters/BooleanToVisibilityConverter.cs @@ -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(); + } +} diff --git a/InstallMonitor/Converters/DateTimeToLocalizedStringConverter.cs b/InstallMonitor/Converters/DateTimeToLocalizedStringConverter.cs new file mode 100644 index 0000000..efdbe2b --- /dev/null +++ b/InstallMonitor/Converters/DateTimeToLocalizedStringConverter.cs @@ -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(); + } + } +} diff --git a/InstallMonitor/InstallMonitor.csproj b/InstallMonitor/InstallMonitor.csproj new file mode 100644 index 0000000..59f5980 --- /dev/null +++ b/InstallMonitor/InstallMonitor.csproj @@ -0,0 +1,32 @@ + + + + WinExe + net8.0-windows + enable + enable + true + schleupengrc_logo.ico + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InstallMonitor/LogEntry.cs b/InstallMonitor/LogEntry.cs new file mode 100644 index 0000000..afb25d6 --- /dev/null +++ b/InstallMonitor/LogEntry.cs @@ -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)); + } +} diff --git a/InstallMonitor/LogParser.cs b/InstallMonitor/LogParser.cs new file mode 100644 index 0000000..de52c9b --- /dev/null +++ b/InstallMonitor/LogParser.cs @@ -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, @"^(?\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; + } + } +} diff --git a/InstallMonitor/LogPhaseGroup.cs b/InstallMonitor/LogPhaseGroup.cs new file mode 100644 index 0000000..5869a85 --- /dev/null +++ b/InstallMonitor/LogPhaseGroup.cs @@ -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 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)); + } +} diff --git a/InstallMonitor/LogReader.cs b/InstallMonitor/LogReader.cs new file mode 100644 index 0000000..43c11ef --- /dev/null +++ b/InstallMonitor/LogReader.cs @@ -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); + } + } +} diff --git a/InstallMonitor/LogTreeView.xaml b/InstallMonitor/LogTreeView.xaml new file mode 100644 index 0000000..99a1d2e --- /dev/null +++ b/InstallMonitor/LogTreeView.xaml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InstallMonitor/LogTreeView.xaml.cs b/InstallMonitor/LogTreeView.xaml.cs new file mode 100644 index 0000000..c22edd5 --- /dev/null +++ b/InstallMonitor/LogTreeView.xaml.cs @@ -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 +{ + /// + /// Interaktionslogik für LogTreeView.xaml + /// + public partial class LogTreeView : Window + { + public LogTreeView(ObservableCollection 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(); + } + } + } + } +} diff --git a/InstallMonitor/MainWindow.xaml b/InstallMonitor/MainWindow.xaml new file mode 100644 index 0000000..ba8b5fc --- /dev/null +++ b/InstallMonitor/MainWindow.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +