Files
mapo-iob-man/IOB-MAN/BlazorForm.cs
T
2025-11-28 08:47:48 +01:00

520 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// <summary>
/// Main Windows Forms host for the Blazor application.
/// This class serves as the entry point for the desktop application, integrating Blazor WebViews
/// with background services, memory monitoring, and auto-restart logic.
///
/// Key Responsibilities:
/// - Initializes and hosts a Blazor WebAssembly UI via Windows Forms BlazorWebView.
/// - Manages lifecycle events: startup, form closing, and shutdown.
/// - Monitors .NET memory usage and system memory to prevent memory leaks or out-of-memory crashes.
/// - Implements periodic checks for auto-restart based on configuration and GC behavior.
/// - Listens to external signals (e.g., config updates, reload/reboot requests) from services.
/// - Gracefully shuts down all managed processes on exit or restart.
/// </summary>
using IOB_MAN.Core.Services;
using Microsoft.AspNetCore.Components.WebView.WindowsForms;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using System.Diagnostics;
using System.Reflection;
namespace IOB_MAN
{
public partial class BlazorForm : Form
{
#region Public Constructors
/// <summary>
/// Initializes a new instance of the BlazorForm class.
/// Sets up the form, initializes services, configures Blazor view, and starts background timers.
/// </summary>
public BlazorForm()
{
InitializeComponent();
ServiceInit();
ConfInit();
InitBlazorView();
StartTimer();
CheckMemory();
}
#endregion Public Constructors
#region Public Methods
/// <summary>
/// Forces a restart of the application with an optional delay.
///
/// Behavior:
/// - If 'force' is true, logs a forced restart (e.g., due to critical error).
/// - Otherwise, triggers restart due to consecutive GC thresholds being exceeded.
/// - Stops all running sub-processes via ACService.DoCloseAll().
/// - Delays restart using cmd.exe timeout to ensure clean exit of current instance.
/// - Uses Application.Exit() to terminate the current process safely.
/// </summary>
/// <param name="force">If true, performs a forced restart; otherwise, triggers restart due to GC exhaustion.</param>
public static void RestartApplication(bool force)
{
if (force)
{
Log.Info($"Chiamata restart application forzato");
}
else
{
Log.Info($"Chiamata restart application x superamento soglia GC consecutivi");
}
string exePath = Application.ExecutablePath;
// Close all managed services (e.g., external SWs) before restart
ACService.DoCloseAll(false);
Log.Info($"Completata chiusura processi x restart");
// Delay restart by 5 seconds using cmd.exe to avoid immediate re-launch
int restartDelay = 5;
ProcessStartInfo psi = new ProcessStartInfo("cmd.exe", $"/C timeout /t {restartDelay} & start \"\" \"{exePath}\"")
{
CreateNoWindow = true,
UseShellExecute = false
};
Process.Start(psi);
Log.Info($"Avviato task di riavvio post delay {restartDelay}sec");
// Exit current instance after delay
Application.Exit();
}
#endregion Public Methods
#region Protected Fields
/// <summary>
/// The next scheduled time for automatic restart check (based on configuration).
/// Used to determine when auto-restart should be triggered.
/// </summary>
protected DateTime tOutAutocheck = DateTime.Now;
#endregion Protected Fields
#region Private Fields
/// <summary>
/// Logger instance for structured logging throughout the application.
/// </summary>
private static Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Counter tracking consecutive GC collections.
/// Used to detect prolonged memory pressure and trigger forced restart if threshold exceeded.
/// </summary>
private int currSeqGC = 0;
/// <summary>
/// Total managed memory allocated by the .NET runtime (in bytes).
/// Monitored to detect memory pressure and trigger GC or restart.
/// </summary>
private long dotNetMemUsed = 0;
/// <summary>
/// Timestamp of the last forced GC collection.
/// Used to enforce periodic GC even if memory is not critically high.
/// </summary>
private DateTime LastForcedGC = DateTime.Now;
/// <summary>
/// Maximum number of consecutive GC collections allowed before triggering restart.
/// Default is 5 — if exceeded, application restarts to prevent memory exhaustion.
/// </summary>
private int maxConsGC = 5;
/// <summary>
/// Maximum allowed memory consumption (in MB) for the application.
/// Default is 128 MB — if managed or total memory exceeds half of this, triggers GC or restart.
/// </summary>
private double maxMemoryLimitMb = 128.0;
/// <summary>
/// Total private memory used by the current process (in bytes).
/// Used to detect system-level memory pressure.
/// </summary>
private long totalMemUsed = 0;
#endregion Private Fields
#region Private Properties
/// <summary>
/// Singleton service for application control (e.g., managing external services, config, restarts).
/// </summary>
private static AppControlService ACService { get; set; } = null!;
/// <summary>
/// Singleton service for handling flux log requests and UI interactions.
/// </summary>
private static FluxLogManService FLMService { get; set; } = null!;
/// <summary>
/// Converts .NET managed memory (bytes) to MB for logging and display.
/// </summary>
private double dotNetMemUsedMb
{
get => dotNetMemUsed / 1024.0 / 1024.0;
}
/// <summary>
/// Stores the last known size of the form to maintain consistent positioning.
/// </summary>
private Size lastSize { get; set; } = new Size(1200, 700);
/// <summary>
/// Converts total process memory (bytes) to MB for logging and display.
/// </summary>
private double totalMemUsedMb
{
get => totalMemUsed / 1024.0 / 1024.0;
}
#endregion Private Properties
#region Private Methods
/// <summary>
/// Executes a restart of the application.
///
/// Behavior:
/// - If 'fullRestart' is true, launches an external restarter (EgwAccRestarter.exe).
/// - Otherwise, uses Application.Restart() to restart the current app (without external update check).
/// </summary>
/// <param name="fullRestart">If true, uses external restarter; otherwise, restarts current app.</param>
private static void DoRestart(bool fullRestart)
{
if (fullRestart)
{
// Launch external restarter (e.g., for complex service restarts)
Assembly assembly = Assembly.GetExecutingAssembly();
string startDir = Path.GetDirectoryName(assembly.Location)!;
string extPath = Path.Combine(startDir, "libs", "EgwAccRestarter.exe");
ProcessStartInfo startInfo = new ProcessStartInfo
{
WindowStyle = ProcessWindowStyle.Normal,
CreateNoWindow = true,
FileName = extPath
};
Process.Start(startInfo);
}
else
{
Application.Restart();
Environment.Exit(0);
}
}
/// <summary>
/// Handles configuration updates from the AppControlService.
///
/// Actions:
/// - Stops and restarts timers to reflect new refresh intervals.
/// - Updates the auto-restart check time from configuration.
/// </summary>
private void ACService_EA_ConfigUpdated()
{
timerUI.Stop();
timerTask.Stop();
tOutAutocheck = ACService.VetoAutoCheck;
StartTimer();
}
/// <summary>
/// Handles a reboot request from the AppControlService.
///
/// Actions:
/// - Stops all timers and services.
/// - Closes all external processes.
/// - Triggers a forced restart.
/// </summary>
private void ACService_EA_RebootRequested()
{
Log.Info("---------------------------------------");
Log.Info("Starting Reboot Request");
Log.Info("---------------------------------------");
timerUI.Stop();
timerUI.Dispose();
timerTask.Stop();
timerTask.Dispose();
ACService.DoCloseAll(false);
RestartApplication(true);
}
/// <summary>
/// Handles a reload request from the AppControlService.
///
/// Action:
/// - Restarts the application without closing external services (partial reload).
/// </summary>
private void ACService_EA_ReloadRequested()
{
DoRestart(false);
}
/// <summary>
/// Handles a restart request from the AppControlService.
///
/// Action:
/// - Triggers a full restart (using external restarter).
/// </summary>
private void ACService_EA_RestartRequested()
{
DoRestart(true);
}
/// <summary>
/// Cleans up event handlers and resources when the form is closing.
///
/// Actions:
/// - Removes event subscriptions from services.
/// - Stops and disposes timers.
/// - Closes all external processes.
/// </summary>
private void BlazorForm_FormClosing(object sender, FormClosingEventArgs e)
{
ACService.EA_ConfigUpdated -= ACService_EA_ConfigUpdated;
ACService.EA_RestartRequested -= ACService_EA_RestartRequested;
ACService.EA_ReloadRequested -= ACService_EA_ReloadRequested;
ACService.EA_RebootRequested -= ACService_EA_RebootRequested;
FLMService.EA_FluxLogReq -= FLMService_EA_FluxLogReq;
timerUI.Stop();
timerUI.Dispose();
timerTask.Stop();
timerTask.Dispose();
ACService.DoCloseAll(false);
}
/// <summary>
/// Sets the form's position to center-bottom of the screen workspace.
/// Ensures consistent placement across different screen configurations.
/// </summary>
private void BlazorForm_Load(object sender, EventArgs e)
{
SetPosition();
}
/// <summary>
/// Periodically checks memory usage and triggers GC if thresholds are exceeded.
///
/// Logic:
/// - Measures managed and total memory.
/// - If managed memory exceeds half of maxMemoryLimitMb OR total memory exceeds maxMemoryLimitMb:
/// - Forces GC collection.
/// - Increments consecutive GC counter.
/// - If counter exceeds maxConsGC → triggers restart.
/// - If more than 1 hour has passed since last forced GC → triggers periodic GC.
/// - Otherwise, decrements counter to prevent overcounting.
/// </summary>
private void CheckMemory()
{
dotNetMemUsed = GC.GetTotalMemory(false);
Process currentProcess = Process.GetCurrentProcess();
totalMemUsed = currentProcess.PrivateMemorySize64;
Log.Info($"Memory Usage | {dotNetMemUsedMb:F2} / {totalMemUsedMb:F2} MB | Managed / Total");
if (dotNetMemUsedMb > maxMemoryLimitMb / 2 || totalMemUsedMb > maxMemoryLimitMb)
{
currSeqGC++;
Log.Info($"Chiamato forzatamente GC.Collect per superamento limite memoria | count: {currSeqGC}");
GC.Collect();
LastForcedGC = DateTime.Now;
if (currSeqGC > maxConsGC)
{
Log.Info("---------------------------------------");
Log.Info($"Superato limite GC consecutivi | count: {currSeqGC}");
RestartApplication(false);
}
}
else if (DateTime.Now.Subtract(LastForcedGC).TotalHours > 1)
{
Log.Info($"Chiamato forzatamente GC.Collect per scadenza periodo");
GC.Collect();
LastForcedGC = DateTime.Now;
}
else
{
if (currSeqGC > 0)
{
currSeqGC--;
Log.Info($"Riduzione contatore GC consecutivi | count: {currSeqGC}");
}
}
}
/// <summary>
/// Initializes configuration values from the AppControlService.
///
/// Specifically: sets maxMemoryLimitMb from configuration (e.g., MaxMemGc).
/// </summary>
private void ConfInit()
{
maxMemoryLimitMb = ACService.MaxMemGc;
}
/// <summary>
/// Handles a request to open a FluxLog editing form.
///
/// Action:
/// - Creates and shows a new FluxLogData form with the provided codIOB.
/// - Enables user to edit or view flux logs for a specific IOB.
/// </summary>
/// <param name="codIOB">The IOB code associated with the requested log.</param>
private void FLMService_EA_FluxLogReq(string codIOB)
{
FluxLogData FldForm = new FluxLogData(FLMService, codIOB);
FldForm.Show();
}
/// <summary>
/// Initializes the Blazor WebView with required services and root components.
///
/// Steps:
/// - Sets up a ServiceCollection with required dependencies.
/// - Registers AppControlService and FluxLogManService as singletons.
/// - Configures the BlazorWebView to load the index.html page.
/// - Adds MainBlazor component to the root element.
/// </summary>
private void InitBlazorView()
{
Stopwatch sw = new Stopwatch();
sw.Start();
try
{
Log.Trace($"TrayMenu OK | {sw.ElapsedMilliseconds}ms");
var services = new ServiceCollection();
Log.Trace($"Add ServiceCollection | {sw.ElapsedMilliseconds}ms");
services.AddWindowsFormsBlazorWebView();
Log.Trace($"Add AddWindowsFormsBlazorWebView | {sw.ElapsedMilliseconds}ms");
services.AddSingleton<AppControlService>(ACService);
services.AddSingleton<FluxLogManService>(FLMService);
Log.Trace($"Add Singleton Services | {sw.ElapsedMilliseconds}ms");
blazorWebView1.HostPage = "wwwroot\\index.html";
blazorWebView1.Services = services.BuildServiceProvider();
blazorWebView1.RootComponents.Add<MainBlazor>("#app");
Log.Trace($"Add MainBlazor | {sw.ElapsedMilliseconds}ms");
}
catch (Exception exc)
{
Log.Error($"Exception during startup{Environment.NewLine}{exc}");
}
}
/// <summary>
/// Initializes core services (AppControlService, FluxLogManService).
///
/// Registers event handlers for:
/// - Config updates
/// - Restart, reload, and reboot requests
/// - FluxLog request events
/// </summary>
private void ServiceInit()
{
ACService = new AppControlService(true);
FLMService = new FluxLogManService();
ACService.EA_ConfigUpdated += ACService_EA_ConfigUpdated;
ACService.EA_RestartRequested += ACService_EA_RestartRequested;
ACService.EA_ReloadRequested += ACService_EA_ReloadRequested;
ACService.EA_RebootRequested += ACService_EA_RebootRequested;
FLMService.EA_FluxLogReq += FLMService_EA_FluxLogReq;
}
/// <summary>
/// Centers the form in the bottom-left of the screen workspace.
/// Ensures consistent UI placement regardless of screen resolution.
/// </summary>
private void SetPosition()
{
Rectangle workArea = Screen.GetWorkingArea(this);
this.Location = new Point((workArea.Right - lastSize.Width) / 2, (workArea.Bottom - lastSize.Height) / 2);
}
/// <summary>
/// Starts background timers based on configuration values from AppControlService.
///
/// Timers:
/// - timerUI: scans application state at intervals (RefreshPeriod).
/// - timerTask: checks for auto-restart conditions (CheckRestartPeriod).
/// - timerMemCheck: monitors memory usage (CheckMemoryPeriod).
///
/// Note: Delays start by 2080ms to avoid race conditions during initialization.
/// </summary>
private void StartTimer()
{
timerUI.Interval = (ACService.RefreshPeriod);
timerTask.Interval = (ACService.CheckRestartPeriod);
timerMemCheck.Interval = (ACService.CheckMemoryPeriod);
timerUI.Start();
Thread.Sleep(rand.Next(20, 80));
timerTask.Start();
Thread.Sleep(rand.Next(20, 80));
timerMemCheck.Start();
}
/// <summary>
/// Random number generator for introducing slight delays in timer startup.
/// Used to prevent all timers from starting simultaneously.
/// </summary>
private Random rand = new Random();
/// <summary>
/// Periodic memory check timer event.
///
/// Triggers CheckMemory() to monitor memory usage and enforce GC if needed.
/// Also logs service statistics for debugging.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void timerMemCheck_Tick(object sender, EventArgs e)
{
CheckMemory();
ACService.PrintStats();
}
/// <summary>
/// Periodic task timer event.
///
/// Actions:
/// - Stops the timer to avoid overlap.
/// - Checks for conditions to auto-restart external services.
/// - Restarts the timer to maintain continuous monitoring.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void timerTask_Tick(object sender, EventArgs e)
{
timerTask.Stop();
ACService.DoAutoRestart(false);
timerTask.Start();
}
/// <summary>
/// Periodic UI scan timer event.
///
/// Actions:
/// - Stops the timer to avoid overlap.
/// - Executes ACService.DoScan() to check application state.
/// - Reinstates the timer to maintain continuous monitoring.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void timerUI_Tick(object sender, EventArgs e)
{
timerUI.Stop();
await ACService.DoScan();
timerUI.Start();
}
#endregion Private Methods
}
}