190 lines
6.2 KiB
C#
190 lines
6.2 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
|
|
namespace Wp2GhostUI;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class ImporterSession
|
|
{
|
|
private const int MaxLines = 3000;
|
|
|
|
private readonly object _gate = new();
|
|
private readonly List<string> _lines = new();
|
|
private readonly StringBuilder _current = new(); // angefangene Zeile (z. B. eine Frage ohne Zeilenende)
|
|
private readonly BlockingCollection<string> _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; }
|
|
/// <summary>Text der offenen Rückfrage; null, solange keine Antwort erwartet wird.</summary>
|
|
public string? Prompt { get; private set; }
|
|
/// <summary>Zählt abgeschlossene Abläufe — die Oberfläche lädt danach ihre Listen neu.</summary>
|
|
public int CompletedRuns { get; private set; }
|
|
|
|
/// <summary>Wird ausgelöst, wenn sich Protokoll, Rückfrage oder Status geändert haben (gebündelt).</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Protokollzeilen plus die gerade angefangene Zeile.</summary>
|
|
public List<string> 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;
|
|
}
|
|
|
|
/// <summary>Startet einen Ablauf im Hintergrund. Es läuft immer nur einer gleichzeitig.</summary>
|
|
public bool Run(string name, Func<Task> 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;
|
|
}
|
|
|
|
/// <summary>Beantwortet die offene Rückfrage.</summary>
|
|
public void Answer(string text)
|
|
{
|
|
if (Prompt == null) return;
|
|
_answers.Add(text);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Beantwortet die offene Rückfrage und gleich die darauf folgenden — z. B. erst "e" (bearbeiten)
|
|
/// und dann die neue Adresse.
|
|
/// </summary>
|
|
public void AnswerMany(params string[] answers)
|
|
{
|
|
if (Prompt == null) return;
|
|
foreach (string answer in answers) _answers.Add(answer);
|
|
}
|
|
|
|
/// <summary>Bricht den laufenden Ablauf bei der nächsten Rückfrage ab (mitten in einem Upload nicht möglich).</summary>
|
|
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();
|
|
}
|
|
}
|