using System.Collections.Concurrent; using System.Text; namespace Wp2GhostUI; /// /// Verbindet den Importer mit der Oberfläche. Der Importer schreibt mit Console.Write und fragt mit /// Console.ReadLine — beides wird hierher umgeleitet: Ausgaben landen im Protokoll, und bei einer /// Rückfrage wartet der Ablauf, bis die Oberfläche eine Antwort liefert. So laufen Konsole und /// Oberfläche garantiert durch dieselbe Logik, ohne dass Abfragen doppelt programmiert werden. /// public class ImporterSession { private const int MaxLines = 3000; private readonly object _gate = new(); private readonly List _lines = new(); private readonly StringBuilder _current = new(); // angefangene Zeile (z. B. eine Frage ohne Zeilenende) private readonly BlockingCollection _answers = new(); private CancellationTokenSource _cancel = new(); private Timer? _notifyTimer; private bool _dirty; public string DataDir { get; private set; } = ""; public bool IsBusy { get; private set; } public string? CurrentTask { get; private set; } /// Text der offenen Rückfrage; null, solange keine Antwort erwartet wird. public string? Prompt { get; private set; } /// Zählt abgeschlossene Abläufe — die Oberfläche lädt danach ihre Listen neu. public int CompletedRuns { get; private set; } /// Wird ausgelöst, wenn sich Protokoll, Rückfrage oder Status geändert haben (gebündelt). public event Action? Changed; public void Start(string dataDir) { DataDir = dataDir; Console.SetOut(new SessionWriter(this)); Console.SetIn(new SessionReader(this)); Importer.Initialize(dataDir); _notifyTimer = new Timer(_ => { if (!_dirty) return; _dirty = false; Changed?.Invoke(); }, null, 150, 150); } /// Protokollzeilen plus die gerade angefangene Zeile. public List GetLines(int last = 400) { lock (_gate) { var result = _lines.Skip(Math.Max(0, _lines.Count - last)).ToList(); if (_current.Length > 0 && Prompt == null) result.Add(_current.ToString()); return result; } } public void ClearLog() { lock (_gate) { _lines.Clear(); _current.Clear(); } _dirty = true; } /// Startet einen Ablauf im Hintergrund. Es läuft immer nur einer gleichzeitig. public bool Run(string name, Func work) { lock (_gate) { if (IsBusy) return false; IsBusy = true; CurrentTask = name; _cancel = new CancellationTokenSource(); while (_answers.TryTake(out _)) { } } Append($"\n▶ {name} ({DateTime.Now:HH:mm:ss})\n"); _ = Task.Run(async () => { try { await work(); Append($"■ {name} beendet\n"); } catch (OperationCanceledException) { Append($"\n■ {name} abgebrochen\n"); } catch (Exception ex) { Append($"\n[Fehler] {ex.Message}\n■ {name} mit Fehler beendet\n"); } finally { lock (_gate) { IsBusy = false; CurrentTask = null; Prompt = null; CompletedRuns++; wp2ghost.LinkQuestion.Current = null; } _dirty = true; } }); return true; } /// Beantwortet die offene Rückfrage. public void Answer(string text) { if (Prompt == null) return; _answers.Add(text); } /// /// Beantwortet die offene Rückfrage und gleich die darauf folgenden — z. B. erst "e" (bearbeiten) /// und dann die neue Adresse. /// public void AnswerMany(params string[] answers) { if (Prompt == null) return; foreach (string answer in answers) _answers.Add(answer); } /// Bricht den laufenden Ablauf bei der nächsten Rückfrage ab (mitten in einem Upload nicht möglich). public void Cancel() { if (!IsBusy) return; _cancel.Cancel(); } internal void Append(string text) { lock (_gate) { foreach (char c in text) { if (c == '\r') continue; if (c != '\n') { _current.Append(c); continue; } _lines.Add(_current.ToString()); _current.Clear(); } if (_lines.Count > MaxLines) _lines.RemoveRange(0, _lines.Count - MaxLines); } _dirty = true; } internal string? WaitForAnswer() { // Außerhalb eines Ablaufs fragt niemand — wie eine geschlossene Eingabe behandeln if (!IsBusy) return null; lock (_gate) { // Die angefangene Zeile ist die Frage; fehlt sie, die letzte vollständige Zeile Prompt = _current.Length > 0 ? _current.ToString() : _lines.LastOrDefault() ?? ""; } _dirty = true; string answer; try { answer = _answers.Take(_cancel.Token); } finally { lock (_gate) { Prompt = null; } } // Antwort wie in der Konsole hinter die Frage schreiben Append(answer + "\n"); return answer; } private sealed class SessionWriter(ImporterSession session) : TextWriter { public override Encoding Encoding => Encoding.UTF8; public override void Write(char value) => session.Append(value.ToString()); public override void Write(string? value) { if (value != null) session.Append(value); } public override void WriteLine(string? value) => session.Append((value ?? "") + "\n"); public override void WriteLine() => session.Append("\n"); } private sealed class SessionReader(ImporterSession session) : TextReader { public override string? ReadLine() => session.WaitForAnswer(); } }