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();
+ }
+ }
+}