From 9fab99112b5e722779cd509feaacfbd48863d30e Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Fri, 8 May 2026 13:46:56 +0200 Subject: [PATCH] =?UTF-8?q?[Fix]=20Sicurezza=20e=20affidabilit=C3=A0=20sto?= =?UTF-8?q?rico=20esecuzioni=20schedulazioni?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SchedulingHistory.razor / .cs: iniettato IWebHostEnvironment per nascondere lo stack trace (con percorsi di file) in produzione; in produzione viene mostrato solo il messaggio di errore sanitizzato e un avviso che invita a consultare i log dell'applicazione; in sviluppo il dettaglio completo resta visibile invariato. - Scheduling.razor.cs (ExecuteScheduleManually): isolata la notifica JS (ShowSuccessMessage / ShowErrorMessage) in un blocco try-catch separato per TaskCanceledException / OperationCanceledException. In questo modo una disconnessione del browser durante un'esecuzione lunga non sovrascrive più il risultato già salvato correttamente come 'success' con uno stato 'failed' e lo stack trace di un'eccezione JSInterop. L'evento viene registrato come avviso di log senza impatto sul record storico. - ScheduledJobService.cs: aggiunto commento esplicativo sul motivo per cui il dettaglio completo (ex.ToString) è salvato nel DB ma la UI ne mostra solo la versione sanitizzata in produzione. --- .../BackgroundServices/ScheduledJobService.cs | 2 ++ Data_Coupler/Pages/Scheduling.razor.cs | 29 ++++++++++++++----- Data_Coupler/Pages/SchedulingHistory.razor | 10 ++++++- Data_Coupler/Pages/SchedulingHistory.razor.cs | 24 +++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/Data_Coupler/BackgroundServices/ScheduledJobService.cs b/Data_Coupler/BackgroundServices/ScheduledJobService.cs index 0068600..bf033a3 100644 --- a/Data_Coupler/BackgroundServices/ScheduledJobService.cs +++ b/Data_Coupler/BackgroundServices/ScheduledJobService.cs @@ -286,6 +286,8 @@ public class ScheduledJobService : BackgroundService executionHistory.EndTime = DateTime.Now; executionHistory.Status = "failed"; executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}"; + // Memorizza il dettaglio completo (stack trace) solo per scopi diagnostici; + // la UI in produzione ne mostrerà una versione sanitizzata senza percorsi di file. executionHistory.ErrorDetails = ex.ToString(); await scheduleService.UpdateExecutionHistoryAsync(executionHistory); } diff --git a/Data_Coupler/Pages/Scheduling.razor.cs b/Data_Coupler/Pages/Scheduling.razor.cs index bef5174..f9d7bcf 100644 --- a/Data_Coupler/Pages/Scheduling.razor.cs +++ b/Data_Coupler/Pages/Scheduling.razor.cs @@ -310,14 +310,21 @@ public partial class Scheduling : ComponentBase : $"Esecuzione fallita: {result.ErrorMessage}"; await ScheduleService.UpdateExecutionStatusAsync(scheduleId, status, message, result.RecordsProcessed); - - if (result.IsSuccess) + + // Notifica l'utente (best-effort: la connessione browser potrebbe essere stata interrotta + // durante un'esecuzione lunga senza che questo invalidi il risultato già salvato). + try { - await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi."); + if (result.IsSuccess) + await ShowSuccessMessage($"Schedulazione eseguita con successo! {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi."); + else + await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}"); } - else + catch (OperationCanceledException) { - await ShowErrorMessage($"Errore durante l'esecuzione: {result.ErrorMessage}"); + // La connessione Blazor è stata interrotta durante l'esecuzione: il risultato è + // già stato salvato correttamente, la notifica non può essere recapitata. + Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser interrotta durante l'esecuzione", scheduleId); } await LoadSchedules(); @@ -326,7 +333,7 @@ public partial class Scheduling : ComponentBase { Logger.LogError(ex, "Errore nell'esecuzione manuale schedulazione {ScheduleId}", scheduleId); - // Aggiorna lo storico in caso di eccezione + // Aggiorna lo storico in caso di eccezione durante l'esecuzione effettiva if (executionHistory != null) { executionHistory.EndTime = DateTime.Now; @@ -337,7 +344,15 @@ public partial class Scheduling : ComponentBase } await ScheduleService.UpdateExecutionStatusAsync(scheduleId, "failed", $"Errore: {ex.Message}"); - await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message); + + try + { + await ShowErrorMessage("Errore nell'esecuzione: " + ex.Message); + } + catch (OperationCanceledException) + { + Logger.LogWarning("Notifica UI non inviata per la schedulazione {ScheduleId}: connessione browser non disponibile", scheduleId); + } } finally { diff --git a/Data_Coupler/Pages/SchedulingHistory.razor b/Data_Coupler/Pages/SchedulingHistory.razor index 3892284..eda0144 100644 --- a/Data_Coupler/Pages/SchedulingHistory.razor +++ b/Data_Coupler/Pages/SchedulingHistory.razor @@ -235,7 +235,15 @@ {
Dettagli Errori
-
@selectedExecution.ErrorDetails
+ @if (IsDevelopment) + { +
@selectedExecution.ErrorDetails
+ } + else + { +

@GetSanitizedErrorMessage(selectedExecution.ErrorDetails)

+ Per i dettagli tecnici completi consultare i log dell'applicazione. + }
} diff --git a/Data_Coupler/Pages/SchedulingHistory.razor.cs b/Data_Coupler/Pages/SchedulingHistory.razor.cs index e84f58c..4cd828e 100644 --- a/Data_Coupler/Pages/SchedulingHistory.razor.cs +++ b/Data_Coupler/Pages/SchedulingHistory.razor.cs @@ -1,6 +1,7 @@ using CredentialManager.Models; using CredentialManager.Services; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; @@ -11,6 +12,29 @@ public partial class SchedulingHistory : ComponentBase [Inject] private IProfileScheduleService ScheduleService { get; set; } = null!; [Inject] private IJSRuntime JSRuntime { get; set; } = null!; [Inject] private ILogger Logger { get; set; } = null!; + [Inject] private IWebHostEnvironment WebHostEnvironment { get; set; } = null!; + + protected bool IsDevelopment => WebHostEnvironment.IsDevelopment(); + + /// + /// Restituisce solo il messaggio dell'eccezione (senza stack trace) per la visualizzazione in produzione. + /// + protected static string GetSanitizedErrorMessage(string errorDetails) + { + if (string.IsNullOrEmpty(errorDetails)) + return string.Empty; + + // Prende solo le righe fino al primo stack frame (riga che inizia con " at") + var lines = errorDetails.Split('\n'); + var messageLines = new System.Collections.Generic.List(); + foreach (var line in lines) + { + if (line.TrimStart().StartsWith("at ", StringComparison.Ordinal)) + break; + messageLines.Add(line.TrimEnd()); + } + return string.Join("\n", messageLines).Trim(); + } protected List? executionHistory; protected ScheduleExecutionHistory? selectedExecution;