using System; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using StarfaceOutlookSync.Models; namespace StarfaceOutlookSync.Services { /// /// 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. /// 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 RemoteNotices = new List(); } /// /// 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 ...). /// public async Task RunAsync(SyncProfile profile, Func> runEngine, Action 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 AcquireCrossClientLock(string dir, Action 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 MyContactsByStarfaceId() { var map = new Dictionary(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; } } }