641267081a
Neue Einstellung "Protokoll auto-leeren - Eintraege aelter als (Tage)". 0 = aus (alle Eintraege bleiben), >0 entfernt aeltere Eintraege. - UserSettings.LogRetentionDays (Standard 0). - Logger.PruneOlderThan(days): parst den Zeitstempel-Prefix je Zeile und entfernt zu alte; Zeilen ohne Zeitstempel bleiben erhalten. - Ausgefuehrt beim Start, vor jedem Sync (Coordinator), beim Oeffnen des Protokolls und beim Speichern der Einstellungen. - SettingsForm: NumericUpDown (0-3650). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
164 lines
6.5 KiB
C#
164 lines
6.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using StarfaceOutlookSync.Models;
|
|
|
|
namespace StarfaceOutlookSync.Services
|
|
{
|
|
/// <summary>
|
|
/// Gemeinsame Orchestrierung fuer JEDEN Sync-Lauf - egal ob automatisch
|
|
/// (Tray/Timer) oder manuell (Fortschrittsfenster). Sorgt einheitlich fuer:
|
|
/// - lokalen Re-Entrancy-Schutz (prozessweit, statisch),
|
|
/// - clientuebergreifende Lock-Datei,
|
|
/// - Anzeige/Logging von Konflikt-Notizen anderer Arbeitsplaetze,
|
|
/// - Protokollierung (Start, Ergebnis, Aenderungen, Konflikte, Fehler),
|
|
/// - Verteilen eigener Konflikt-Notizen.
|
|
/// Die eigentliche UI (Tray-Meldung vs. Fenster) bleibt beim Aufrufer.
|
|
/// </summary>
|
|
public class SyncCoordinator
|
|
{
|
|
// Prozessweit: nie zwei Syncs gleichzeitig - auch nicht manuell + automatisch.
|
|
private static int _running = 0;
|
|
|
|
private readonly ProfileManager _profileManager = new ProfileManager();
|
|
private readonly ConflictNotifier _notifier = new ConflictNotifier();
|
|
|
|
public class SyncOutcome
|
|
{
|
|
public bool Skipped;
|
|
public string SkipReason = "";
|
|
public SyncResult Result;
|
|
public Exception Error;
|
|
public List<ConflictNotice> RemoteNotices = new List<ConflictNotice>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fuehrt einen Sync koordiniert aus. runEngine kapselt den eigentlichen
|
|
/// Engine-Aufruf (inkl. ggf. Task.Run/Progress-Anbindung des Aufrufers),
|
|
/// damit das Threading-Verhalten pro Pfad erhalten bleibt. status erhaelt
|
|
/// Fortschrittstexte (Warten auf Sperre, Synchronisiere ...).
|
|
/// </summary>
|
|
public async Task<SyncOutcome> RunAsync(SyncProfile profile,
|
|
Func<Task<SyncResult>> runEngine, Action<string> status)
|
|
{
|
|
var outcome = new SyncOutcome();
|
|
|
|
if (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
|
|
{
|
|
outcome.Skipped = true;
|
|
outcome.SkipReason = "Sync laeuft bereits, bitte warten...";
|
|
return outcome;
|
|
}
|
|
|
|
SyncLock crossLock = null;
|
|
var settings = UserSettings.Load();
|
|
Logger.PruneOlderThan(settings.LogRetentionDays);
|
|
var sharedDir = settings.SharedDirectory;
|
|
try
|
|
{
|
|
crossLock = await AcquireCrossClientLock(sharedDir, status);
|
|
if (crossLock == null)
|
|
{
|
|
outcome.Skipped = true;
|
|
outcome.SkipReason = "Anderer Arbeitsplatz synchronisiert gerade - uebersprungen.";
|
|
Logger.Log($"Sync '{profile.Name}' uebersprungen: anderer Arbeitsplatz synct gerade.");
|
|
return outcome;
|
|
}
|
|
|
|
// Konflikt-Hinweise anderer Arbeitsplaetze ermitteln, BEVOR der Sync
|
|
// den eigenen Stand auf den Gewinner-Wert angleicht.
|
|
try
|
|
{
|
|
outcome.RemoteNotices = _notifier.GetPending(sharedDir, MyContactsByStarfaceId());
|
|
foreach (var n in outcome.RemoteNotices)
|
|
Logger.Log($" KONFLIKT (anderer Arbeitsplatz): {n}");
|
|
}
|
|
catch { }
|
|
|
|
Logger.Log($"Sync gestartet: '{profile.Name}' (Richtung: {profile.SyncDirection})");
|
|
status?.Invoke($"Synchronisiere '{profile.Name}'...");
|
|
|
|
SyncResult result;
|
|
try
|
|
{
|
|
result = await runEngine();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
outcome.Error = ex;
|
|
Logger.Log($"Sync FEHLER '{profile.Name}': {ex.Message}");
|
|
return outcome;
|
|
}
|
|
outcome.Result = result;
|
|
|
|
var msg = $"{profile.Name}: {result.Created} erstellt, {result.Updated} aktualisiert"
|
|
+ (result.Errors > 0 ? $", {result.Errors} Fehler" : "")
|
|
+ (result.Conflicts.Count > 0 ? $", {result.Conflicts.Count} Konflikt(e)" : "");
|
|
|
|
Logger.Log($"Sync fertig: {msg}");
|
|
foreach (var ch in result.Changes) Logger.Log($" {ch}");
|
|
foreach (var em in result.ErrorMessages) Logger.Log($" Fehler: {em}");
|
|
foreach (var c in result.Conflicts) Logger.Log($" KONFLIKT: {c}");
|
|
|
|
// Andere Arbeitsplaetze ueber eigene Konflikte informieren.
|
|
if (result.Conflicts.Count > 0)
|
|
_notifier.Write(sharedDir, result.Conflicts);
|
|
|
|
return outcome;
|
|
}
|
|
finally
|
|
{
|
|
crossLock?.Dispose();
|
|
Interlocked.Exchange(ref _running, 0);
|
|
}
|
|
}
|
|
|
|
private async Task<SyncLock> AcquireCrossClientLock(string dir, Action<string> status)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dir))
|
|
return SyncLock.NoOp();
|
|
|
|
try
|
|
{
|
|
// Legt das Verzeichnis bei Bedarf an (idempotent). Funktioniert mit
|
|
// UNC-Pfaden (\\server\freigabe\...) - kein Netzlaufwerk noetig.
|
|
Directory.CreateDirectory(dir);
|
|
}
|
|
catch
|
|
{
|
|
status?.Invoke("Gemeinsames Verzeichnis nicht erreichbar - synce ohne Sperre.");
|
|
return SyncLock.NoOp();
|
|
}
|
|
|
|
var deadline = DateTime.UtcNow.AddMinutes(2);
|
|
int attempt = 0;
|
|
while (true)
|
|
{
|
|
var l = SyncLock.TryAcquire(dir, out var heldBy);
|
|
if (l != null) return l;
|
|
if (DateTime.UtcNow >= deadline) return null;
|
|
|
|
attempt++;
|
|
status?.Invoke($"Warte auf anderen Arbeitsplatz ({heldBy ?? "unbekannt"})... ({attempt})");
|
|
await Task.Delay(3000);
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, UnifiedContact> MyContactsByStarfaceId()
|
|
{
|
|
var map = new Dictionary<string, UnifiedContact>(StringComparer.OrdinalIgnoreCase);
|
|
try
|
|
{
|
|
foreach (var p in _profileManager.GetProfiles())
|
|
foreach (var m in _profileManager.GetMappings(p.Id))
|
|
if (!string.IsNullOrEmpty(m.StarfaceId) && !map.ContainsKey(m.StarfaceId))
|
|
map[m.StarfaceId] = m.LastOutlook;
|
|
}
|
|
catch { }
|
|
return map;
|
|
}
|
|
}
|
|
}
|