v0.22.0 — Health checks editor : icon picker + per-check refresh
Settings → Avancés → Bandeau de santé : liste éditable des checks (Add / Edit / Delete) avec dialog modal pour chaque entry. Picker d'emoji parmi 28 icônes curatées (VR / streaming / réseau / système). Édition du nom, type (Process / Ping), cible, intervalle de refresh en ms (per-check), et seuils Ping (warn / error / timeout) sous une section Avancé. La config est persistée dans %LocalAppData%\PSLauncher\config.json donc elle survit aux auto-updates du launcher. Modifiable aussi à la main dans le json pour les power-users. Le bandeau hot-reload après Save : cancel des boucles de polling existantes, reconstruction des HealthIndicators depuis la config fraîche, puis relance d'une boucle dédiée par check (chacune cadencée sur son propre RefreshIntervalMs — Process check rapide peut tourner à 1 s, Ping reste à 5-10 s). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,14 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
public ObservableCollection<HealthIndicatorViewModel> HealthIndicators { get; } = new();
|
||||
public bool HasHealthIndicators => HealthIndicators.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Cancelle les boucles de polling per-check actuellement en vol. Recréée à
|
||||
/// chaque appel à <see cref="StartHealthLoop"/> pour permettre un redémarrage
|
||||
/// propre après édition des checks dans Settings (hot-reload sans relancer
|
||||
/// l'app).
|
||||
/// </summary>
|
||||
private CancellationTokenSource? _healthLoopCts;
|
||||
|
||||
/// <summary>
|
||||
/// Le bandeau santé n'a de sens que sur la page Library — Reports et
|
||||
/// Documentation sont des WebView pleine-page où afficher des pills
|
||||
@@ -319,41 +327,53 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lance une tâche en arrière-plan qui exécute tous les checks en parallèle,
|
||||
/// applique les résultats sur les VM via le dispatcher UI, puis attend
|
||||
/// RefreshIntervalSeconds avant de recommencer. La tâche tourne tant que
|
||||
/// l'app vit ; pas de cancel explicite (l'arrêt suit la fermeture du process).
|
||||
/// Lance une boucle indépendante par check, chacune cadencée sur son propre
|
||||
/// <see cref="HealthCheckEntry.RefreshIntervalMs"/>. Un Process check rapide
|
||||
/// (~1 ms) peut tourner toutes les 1-2 s sans impact CPU, alors qu'un Ping
|
||||
/// (qui coûte un round-trip réseau) tourne typiquement à 5-10 s. Premier
|
||||
/// passage immédiat puis re-check selon l'intervalle.
|
||||
///
|
||||
/// Cancelle la boucle précédente si elle existe (cas du hot-reload depuis
|
||||
/// Settings → édition des checks).
|
||||
/// </summary>
|
||||
private void StartHealthLoop()
|
||||
{
|
||||
// Stop l'ancienne boucle si on est en hot-reload (édition Settings)
|
||||
try { _healthLoopCts?.Cancel(); } catch { /* déjà disposée */ }
|
||||
_healthLoopCts?.Dispose();
|
||||
_healthLoopCts = null;
|
||||
|
||||
if (HealthIndicators.Count == 0) return;
|
||||
var interval = TimeSpan.FromSeconds(Math.Max(2, _config.HealthChecks.RefreshIntervalSeconds));
|
||||
_ = Task.Run(async () =>
|
||||
|
||||
_healthLoopCts = new CancellationTokenSource();
|
||||
var ct = _healthLoopCts.Token;
|
||||
|
||||
foreach (var vm in HealthIndicators.ToList())
|
||||
{
|
||||
while (true)
|
||||
var localVm = vm;
|
||||
// Floor de 500 ms pour éviter qu'un user mette 0/1 ms par erreur dans le json
|
||||
// et sature un coeur CPU à pleins gaz.
|
||||
var intervalMs = Math.Max(500, localVm.Entry.RefreshIntervalMs);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
var snapshot = HealthIndicators.ToList();
|
||||
var tasks = snapshot.Select(async vm =>
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _healthService.RunCheckAsync(vm.Entry, CancellationToken.None);
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => vm.Apply(result));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Health check {Name} threw", vm.Entry.Name);
|
||||
}
|
||||
});
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
var result = await _healthService.RunCheckAsync(localVm.Entry, ct).ConfigureAwait(false);
|
||||
if (ct.IsCancellationRequested) break;
|
||||
await Application.Current.Dispatcher.InvokeAsync(() => localVm.Apply(result));
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Health check {Name} threw", localVm.Entry.Name);
|
||||
}
|
||||
try { await Task.Delay(intervalMs, ct).ConfigureAwait(false); }
|
||||
catch { break; }
|
||||
}
|
||||
catch (Exception ex) { _logger.LogDebug(ex, "Health loop tick"); }
|
||||
try { await Task.Delay(interval).ConfigureAwait(false); }
|
||||
catch { break; }
|
||||
}
|
||||
});
|
||||
}, ct);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -546,6 +566,15 @@ public sealed partial class MainViewModel : ObservableObject
|
||||
_license = _licenseService.GetCached();
|
||||
NotifyLicenseChanged();
|
||||
RebuildList();
|
||||
|
||||
// Hot-reload du bandeau de santé : la liste des checks (et leurs intervals)
|
||||
// a pu être éditée. On reconstruit les indicateurs et on relance la boucle
|
||||
// per-check sans avoir à fermer/rouvrir le launcher.
|
||||
InitHealthIndicators();
|
||||
StartHealthLoop();
|
||||
OnPropertyChanged(nameof(ReportUri));
|
||||
OnPropertyChanged(nameof(DocumentationUri));
|
||||
OnPropertyChanged(nameof(HasDocumentationUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user