From 2df356dd07f8a5f369eea014bf62486051017be0 Mon Sep 17 00:00:00 2001 From: BtbN Date: Wed, 27 Jul 2022 19:06:40 +0200 Subject: [PATCH] Implement actual logic --- ESAWindowTracker/App.xaml.cs | 79 +++-- ESAWindowTracker/ESAWindowTracker.csproj | 3 + ESAWindowTracker/MainWindow.xaml | 9 + ESAWindowTracker/MainWindow.xaml.cs | 28 +- .../PublishProfiles/FolderProfile.pubxml | 17 ++ ESAWindowTracker/Rabbit.cs | 282 ++++++++++++++++++ ESAWindowTracker/WindowTracker.cs | 133 +++++++++ 7 files changed, 528 insertions(+), 23 deletions(-) create mode 100644 ESAWindowTracker/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 ESAWindowTracker/Rabbit.cs create mode 100644 ESAWindowTracker/WindowTracker.cs diff --git a/ESAWindowTracker/App.xaml.cs b/ESAWindowTracker/App.xaml.cs index 0b4d7a0..a288fed 100644 --- a/ESAWindowTracker/App.xaml.cs +++ b/ESAWindowTracker/App.xaml.cs @@ -10,51 +10,88 @@ using System.Windows; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Debug; +using Microsoft.Extensions.Logging.Console; namespace ESAWindowTracker { - /// - /// Interaction logic for App.xaml - /// public partial class App : Application { - public IServiceProvider ServiceProvider { get; set; } - public IConfiguration Configuration { get; set; } + private readonly IHost host; + + new public static App Current => (App)Application.Current; public App() { - var builder = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); - + host = new HostBuilder() + .ConfigureAppConfiguration((context, builder) => + { + builder.SetBasePath(AppDomain.CurrentDomain.BaseDirectory); + builder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); +#if DEBUG + builder.AddUserSecrets(); +#endif + }).ConfigureServices((context, services) => + { + ConfigureServices(context.Configuration, services); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(); #if DEBUG - builder.AddUserSecrets(); + logging.AddDebug(); +#else + logging.AddEventLog(); #endif + }) + .Build(); - Configuration = builder.Build(); WriteConfig(); - var serviceCollection = new ServiceCollection(); - ConfigureServices(serviceCollection); - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - ServiceProvider = serviceCollection.BuildServiceProvider(); } - private void ConfigureServices(ServiceCollection services) + private void ConfigureServices(IConfiguration configuration, IServiceCollection services) { - services.AddTransient(); + services.AddLogging(); + + services.AddOptions(); + services.Configure(configuration); + services.Configure(configuration.GetSection("RabbitConfig")); + + RabbitService.Register(services); + WindowTracker.Register(services); + + services.AddSingleton(); + } + + protected override async void OnStartup(StartupEventArgs e) + { + await host.StartAsync(); + host.Services.GetRequiredService(); + base.OnStartup(e); } - protected override void OnStartup(StartupEventArgs e) + protected override async void OnExit(ExitEventArgs e) { - ServiceProvider.GetRequiredService(); + using (host) + { + await host.StopAsync(TimeSpan.FromSeconds(5)); + } + + base.OnExit(e); } public void WriteConfig(Config? config = null) { - if (config == null) - config = Configuration.Get() ?? new Config(); + if (config == null) { + using var scope = host.Services.CreateScope(); + config = scope.ServiceProvider.GetRequiredService>().Value; + } var jsonWriteOptions = new JsonSerializerOptions() { diff --git a/ESAWindowTracker/ESAWindowTracker.csproj b/ESAWindowTracker/ESAWindowTracker.csproj index 2b889fe..8f02ff3 100644 --- a/ESAWindowTracker/ESAWindowTracker.csproj +++ b/ESAWindowTracker/ESAWindowTracker.csproj @@ -19,6 +19,9 @@ + + + diff --git a/ESAWindowTracker/MainWindow.xaml b/ESAWindowTracker/MainWindow.xaml index 72ac3f5..141053d 100644 --- a/ESAWindowTracker/MainWindow.xaml +++ b/ESAWindowTracker/MainWindow.xaml @@ -15,6 +15,13 @@ /> + + + + + + + @@ -24,5 +31,7 @@ + + diff --git a/ESAWindowTracker/MainWindow.xaml.cs b/ESAWindowTracker/MainWindow.xaml.cs index 9fbe378..e1dffc3 100644 --- a/ESAWindowTracker/MainWindow.xaml.cs +++ b/ESAWindowTracker/MainWindow.xaml.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -21,9 +23,31 @@ namespace ESAWindowTracker /// public partial class MainWindow : Window { - public MainWindow() + private readonly IOptionsMonitor options; + private readonly RabbitMessageSender rabbitMessageSender; + + public MainWindow(IOptionsMonitor options, RabbitMessageSender rabbitMessageSender) { InitializeComponent(); + + this.options = options; + this.rabbitMessageSender = rabbitMessageSender; + + rabbitMessageSender.StatusChanged += RabbitMessageSender_StatusChanged; + RabbitStatusLabel.Content = rabbitMessageSender.Status; + + options.OnChange(OnConfigChange); + OnConfigChange(options.CurrentValue, ""); + } + + private void OnConfigChange(Config cfg, string _) + { + IDField.Content = $"This is PC {cfg.PCID} at {cfg.EventShort}"; + } + + private void RabbitMessageSender_StatusChanged(string status) + { + RabbitStatusLabel.Content = status; } private void Show_Executed(object sender, ExecutedRoutedEventArgs e) diff --git a/ESAWindowTracker/Properties/PublishProfiles/FolderProfile.pubxml b/ESAWindowTracker/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..1c6e548 --- /dev/null +++ b/ESAWindowTracker/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\Release\net6.0-windows\publish\win-x64\ + FileSystem + net6.0-windows + win-x64 + false + true + false + + \ No newline at end of file diff --git a/ESAWindowTracker/Rabbit.cs b/ESAWindowTracker/Rabbit.cs new file mode 100644 index 0000000..bc23307 --- /dev/null +++ b/ESAWindowTracker/Rabbit.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Security.Authentication; +using System.Text.Json.Serialization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using RabbitMQ.Client; +using System.Text.Json; + +namespace ESAWindowTracker +{ + public class RabbitMessage + { + [JsonPropertyName("event_short")] + public string? Eventshort { get; set; } + + [JsonPropertyName("pc_id")] + public string? PCID { get; set; } + + [JsonPropertyName("window_title")] + public string? WindowTitle { get; set; } + + [JsonPropertyName("window_left")] + public int WindowLeft { get; set; } + [JsonPropertyName("window_right")] + public int WindowRight { get; set; } + + [JsonPropertyName("window_top")] + public int WindowTop { get; set; } + [JsonPropertyName("window_bottom")] + public int WindowBottom { get; set; } + } + + public class RabbitMessageSender + { + public event Action? OnRabbitMessage; + + public void PostMesage(RabbitMessage message) + { + OnRabbitMessage?.Invoke(message); + } + + private string status = ""; + public string Status + { + get => status; + set + { + status = value; + StatusChanged?.Invoke(status); + } + } + public event Action? StatusChanged; + } + + public class RabbitService : IHostedService + { + public static void Register(IServiceCollection services) + { + services.AddSingleton(); + services.AddHostedService(); + } + + private readonly ILogger logger; + private readonly IOptionsMonitor options; + private readonly RabbitMessageSender msg_sender; + + private IConnection? mqCon; + private IModel? channel; + + public RabbitService(ILogger logger, IOptionsMonitor options, RabbitMessageSender msg_sender) + { + this.logger = logger; + this.options = options; + this.msg_sender = msg_sender; + } + + private Task CloseAsync(CancellationToken cancellationToken) + { + return Task.Run(() => + { + msg_sender.OnRabbitMessage -= OnRabbitMessage; + + if (channel != null) + { + channel.Close(); + channel = null; + } + if (mqCon != null) + { + mqCon.Close(); + mqCon = null; + } + }, cancellationToken); + } + + private RabbitConfig inUseOptions = new(); + + private ConnectionFactory GetConnFac() + { + RabbitConfig opts = options.CurrentValue.RabbitConfig; + inUseOptions = opts; + + var factory = new ConnectionFactory + { + HostName = opts.Host, + VirtualHost = opts.VHost, + Port = opts.Port, + + UserName = opts.User, + Password = opts.Pass, + + AutomaticRecoveryEnabled = true, + NetworkRecoveryInterval = TimeSpan.FromSeconds(10), + + DispatchConsumersAsync = true + }; + + factory.Ssl.Enabled = opts.Tls; + factory.Ssl.Version = SslProtocols.Tls12 | SslProtocols.Tls13; + factory.Ssl.ServerName = factory.HostName; + + return factory; + } + + private void InitRabbitMQ() + { + if (channel != null) + channel.Close(); + if (mqCon != null) + mqCon.Close(); + + channel = null; + mqCon = null; + + mqCon = GetConnFac().CreateConnection(); + + channel = mqCon.CreateModel(); + channel.BasicQos(0, 1, false); + + channel.ExchangeDeclare("cg", ExchangeType.Topic, true, true); + + logger.LogInformation("Connected to MQ service at {0}.", mqCon.Endpoint.HostName); + msg_sender.Status = $"Connected to MQ service at {mqCon.Endpoint.HostName}."; + } + + private async Task SetupRabbitConsumers(CancellationToken cancellationToken) + { + try + { + await Task.Run(() => + { + InitRabbitMQ(); + }, cancellationToken); + + msg_sender.OnRabbitMessage += OnRabbitMessage; + + logger.LogInformation("Rabbit up and running."); + } + catch (Exception e) + { + logger.LogWarning($"Failed establishing Rabbit connection: {e.Message}"); + msg_sender.Status = $"Failed establishing Rabbit connection: {e.Message}"; + } + } + + private readonly SemaphoreSlim rabbitLock = new SemaphoreSlim(1); + + private async void OnRabbitMessage(RabbitMessage msg) + { + if (await rabbitLock.WaitAsync(10000)) + { + try + { + if (channel == null) + throw new Exception("No channel to send message to."); + + var cfg = options.CurrentValue; + string msg_json = JsonSerializer.Serialize(msg); + + await Task.Run(() => + { + channel.BasicPublish( + "cg", + $"{cfg.EventShort}.{cfg.PCID}.window_info_changed", + null, + Encoding.UTF8.GetBytes(msg_json)); + }); + } + catch (Exception e) + { + logger.LogError(e, "Failed sending rabbit message."); + msg_sender.Status = $"Failed sending rabbit message: {e.Message}"; + } + finally + { + rabbitLock.Release(); + } + } + } + + private async void OnOptionsChanged(Config opts) + { + if (await rabbitLock.WaitAsync(10000)) + { + try + { + if (inUseOptions == null || OptionsEqual(inUseOptions, opts.RabbitConfig)) + { + logger.LogInformation("Rabbit config unchanged."); + return; + } + + logger.LogInformation("Reloading rabbit config."); + await CloseAsync(CancellationToken.None); + await SetupRabbitConsumers(CancellationToken.None); + } + catch (Exception e) + { + logger.LogError(e, "Failed connecting to Rabbit."); + msg_sender.Status = $"Failed connecting to Rabbit: {e.Message}"; + } + finally + { + rabbitLock.Release(); + } + } + } + + private static bool OptionsEqual(RabbitConfig a, RabbitConfig b) + { + return a.Host == b.Host + && a.VHost == b.VHost + && a.Port == b.Port + && a.Tls == b.Tls + && a.User == b.User + && a.Pass == b.Pass; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + msg_sender.Status = "Starting up..."; + + try + { + await SetupRabbitConsumers(cancellationToken); + } + catch (Exception e) + { + logger.LogError(e, "Failed connecting to Rabbit."); + } + + optionsChangeListener = options.OnChange(opts => OnOptionsChanged(opts)); + } + + IDisposable? optionsChangeListener = null; + + public async Task StopAsync(CancellationToken cancellationToken) + { + logger.LogInformation("Stopping Rabbit Listener."); + msg_sender.Status = "Stopping..."; + + if (optionsChangeListener != null) + { + optionsChangeListener.Dispose(); + optionsChangeListener = null; + } + + await CloseAsync(cancellationToken); + + logger.LogInformation("Stopped Rabbit Listener."); + msg_sender.Status = "Stopped."; + } + } +} diff --git a/ESAWindowTracker/WindowTracker.cs b/ESAWindowTracker/WindowTracker.cs new file mode 100644 index 0000000..e3b130a --- /dev/null +++ b/ESAWindowTracker/WindowTracker.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ESAWindowTracker +{ + internal class WindowTracker : IHostedService, IDisposable + { + public static void Register(IServiceCollection services) + { + services.AddHostedService(); + } + + private readonly ILogger logger; + private readonly IOptionsMonitor options; + private readonly RabbitMessageSender msgSender; + private Timer? timer = null; + + public WindowTracker(ILogger logger, IOptionsMonitor options, RabbitMessageSender msgSender) + { + this.logger = logger; + this.options = options; + this.msgSender = msgSender; + } + + public Task StartAsync(CancellationToken stoppingToken) + { + logger.LogInformation("WindowTracker Service running."); + timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + return Task.CompletedTask; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [DllImport("user32.dll", SetLastError = true)] + static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll", SetLastError = true)] + static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect); + [DllImport("user32.dll", SetLastError = true)] + static extern bool ClientToScreen(IntPtr hWnd, ref System.Drawing.Point lpPoint); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern int GetWindowTextLength(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + private void DoWork(object? _) + { + var hwnd = GetForegroundWindow(); + if (hwnd == IntPtr.Zero) + { + logger.LogError("Failed getting foreground window."); + return; + } + + if (!GetClientRect(hwnd, out RECT clientrect)) + { + logger.LogError("Failed getting client rect."); + return; + } + + Point top_left = new Point(clientrect.Left, clientrect.Top); + Point bottom_right = new Point(clientrect.Right, clientrect.Bottom); + + if (!ClientToScreen(hwnd, ref top_left)) + { + logger.LogError("Failed converting client to screen."); + return; + } + if (!ClientToScreen(hwnd, ref bottom_right)) + { + logger.LogError("Failed converting client to screen."); + return; + } + + StringBuilder titleBuilder = new StringBuilder(GetWindowTextLength(hwnd) + 1); + if (GetWindowText(hwnd, titleBuilder, titleBuilder.Capacity) <= 0) + { + logger.LogError("Failed getting Window Title."); + return; + } + +#if DEBUG + logger.LogInformation($"Rect for {titleBuilder}: {top_left.X},{top_left.Y},{bottom_right.X},{bottom_right.Y}"); +#endif + + var opts = options.CurrentValue; + + RabbitMessage msg = new RabbitMessage + { + PCID = opts.PCID, + Eventshort = opts.EventShort, + + WindowTitle = titleBuilder.ToString(), + + WindowLeft = top_left.X, + WindowTop = top_left.Y, + + WindowRight = bottom_right.X, + WindowBottom = bottom_right.Y + }; + + msgSender.PostMesage(msg); + } + + public Task StopAsync(CancellationToken stoppingToken) + { + logger.LogInformation("WindowTracker Service is stopping."); + timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + public void Dispose() + { + timer?.Dispose(); + } + } +}