R4: Worker von WorkerEngine auf IHostedService (Generic Host)

- WorkerBase implementiert IHostedService (StartAsync/StopAsync mit CancellationToken,
  eigener CTS, Guard auf Info.Active); IWorker auf Metadaten + TriggerAsync reduziert
- Worker via AddHostedService registriert (Core + Modul); AppHost.Start() startet sie,
  AppHost.StopAsync() stoppt sie
- WorkerEngine auf leichte Registry reduziert (WorkerInfos fuer UI + TriggerWorkerAsync)
- LauncherForm startet/stoppt keine Worker mehr (Host-getrieben)
- Hosting.Abstractions im Core; Tests angepasst -> 43/43 gruen; smoke-ui + App-Start ok

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-28 18:39:14 +02:00
parent 475c0b41cb
commit 19dac18050
9 changed files with 67 additions and 106 deletions
+12 -73
View File
@@ -5,92 +5,31 @@ using IBKRTrader.Core.Logging;
namespace IBKRTrader.Core.Workers;
/// <summary>
/// Orchestriert alle registrierten IWorker.
/// Startet/stoppt Worker
/// Stellt BindingList für DataGridView bereit
/// Thread-sicher via ConcurrentDictionary
/// Leichtgewichtige Registry über alle Worker/Services. Start/Stop übernimmt der Generic Host
/// (jeder Worker ist ein <see cref="Microsoft.Extensions.Hosting.IHostedService"/>). Diese Klasse
/// stellt die live-bindbare Liste für die Workers-Ansicht bereit und erlaubt den manuellen Trigger.
/// </summary>
public class WorkerEngine
{
private readonly IEnumerable<IWorker> _workers;
private readonly LoggingService _logger;
private readonly ConcurrentDictionary<string, IWorker> _registry = new();
private CancellationTokenSource _cts = new();
private readonly ConcurrentDictionary<string, IWorker> _registry = new();
/// <summary>Live-bindbare Liste für dgv_workerlist.</summary>
/// <summary>Live-bindbare Liste für die Workers-Ansicht (DataGridView).</summary>
public BindingList<WorkerInfo> WorkerInfos { get; } = [];
public WorkerEngine(IEnumerable<IWorker> workers, LoggingService logger)
{
_workers = workers;
_logger = logger;
}
// ─── Engine-Lifecycle ─────────────────────────────────────────────────────
public async Task StartAllAsync()
{
_cts = new CancellationTokenSource();
foreach (var worker in _workers)
foreach (var worker in workers)
{
RegisterWorker(worker);
if (worker.Info.Active)
await worker.StartAsync(_cts.Token);
_registry[worker.Name] = worker;
WorkerInfos.Add(worker.Info);
}
_logger.Info("Core", $"WorkerEngine gestartet {_registry.Count} Worker/Services registriert.");
}
public async Task StopAllAsync()
{
await _cts.CancelAsync();
var tasks = _registry.Values.Select(w => w.StopAsync());
await Task.WhenAll(tasks);
_logger.Info("Core", "WorkerEngine gestoppt.");
}
// ─── Einzelsteuerung ──────────────────────────────────────────────────────
public async Task StartWorkerAsync(string name)
{
if (!_registry.TryGetValue(name, out var worker)) return;
worker.Info.Active = true;
await worker.StartAsync(_cts.Token);
_logger.Info("Core", $"Worker manuell gestartet: {name}");
}
public async Task StopWorkerAsync(string name)
{
if (!_registry.TryGetValue(name, out var worker)) return;
worker.Info.Active = false;
await worker.StopAsync();
_logger.Info("Core", $"Worker manuell gestoppt: {name}");
logger.Info("Core", $"WorkerEngine: {_registry.Count} Worker/Services registriert.");
}
/// <summary>Löst einen sofortigen manuellen Run des benannten Workers aus.</summary>
public async Task TriggerWorkerAsync(string name)
{
if (!_registry.TryGetValue(name, out var worker)) return;
await worker.TriggerAsync();
_logger.Info("Core", $"Worker manuell ausgelöst: {name}");
}
// ─── Intern ───────────────────────────────────────────────────────────────
private void RegisterWorker(IWorker worker)
{
_registry[worker.Name] = worker;
// WorkerInfo in BindingList eintragen (UI-Thread, falls nötig)
if (WorkerInfos is { } list)
{
if (SynchronizationContext.Current != null)
list.Add(worker.Info);
else
list.Add(worker.Info);
}
if (_registry.TryGetValue(name, out var worker))
await worker.TriggerAsync();
}
}