using System; using System.Diagnostics; using System.IO; using System.Text; namespace StarfaceOutlookSync.Services { /// /// Clientuebergreifende Sperre ueber eine Lock-Datei in einem gemeinsamen /// Verzeichnis (Netzlaufwerk/Share). Verhindert, dass mehrere Arbeitsplaetze /// gleichzeitig dasselbe Starface-Adressbuch synchronisieren. /// /// Atomar ueber FileMode.CreateNew: das Anlegen schlaegt fehl, wenn die Datei /// schon existiert. Die Datei wird waehrend des Syncs offen gehalten /// (FileShare.None) - stuerzt ein Client ab, gibt das Betriebssystem das /// Handle frei und ein anderer Client kann die (dann verwaiste) Datei /// uebernehmen. Eine reine Zeitstempel-Pruefung allein waere nicht sicher. /// public sealed class SyncLock : IDisposable { public const string LockFileName = "starface-sync.lock"; // Aelter als das -> als verwaist behandeln und Uebernahme VERSUCHEN. // Lebt der Eigentuemer noch, schlaegt das Loeschen am offenen Handle fehl, // die Sperre bleibt also korrekt bestehen. private static readonly TimeSpan StaleAfter = TimeSpan.FromMinutes(15); private FileStream _stream; private readonly string _path; private SyncLock(string path, FileStream stream) { _path = path; _stream = stream; } /// Sperre, die nichts haelt (wenn kein gemeinsames Verzeichnis konfiguriert/erreichbar ist). public static SyncLock NoOp() => new SyncLock(null, null); /// /// Versucht die Sperre zu holen. Gibt null zurueck, wenn gerade ein /// anderer (lebender) Client synct. heldBy enthaelt - sofern lesbar - /// Infos zum aktuellen Halter. /// public static SyncLock TryAcquire(string dir, out string heldBy) { heldBy = null; var path = Path.Combine(dir, LockFileName); var lockObj = TryCreate(path); if (lockObj != null) return lockObj; // Existiert bereits. Halter ermitteln und ggf. verwaiste Datei uebernehmen. heldBy = ReadOwner(path); if (IsStale(path)) { try { File.Delete(path); } // scheitert, wenn der Halter noch lebt (Handle offen) catch { return null; } return TryCreate(path); } return null; } private static SyncLock TryCreate(string path) { try { var fs = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None); var info = $"{Environment.MachineName}|{Environment.UserName}|{DateTime.UtcNow:o}|pid{GetPid()}"; var bytes = Encoding.UTF8.GetBytes(info); fs.Write(bytes, 0, bytes.Length); fs.Flush(); return new SyncLock(path, fs); } catch (IOException) { return null; } // existiert schon catch (UnauthorizedAccessException) { return null; } } private static int GetPid() { try { return Process.GetCurrentProcess().Id; } catch { return 0; } } private static string ReadOwner(string path) { try { // Eigene Freigabe, falls der Halter die Datei nur zum Schreiben offen hat. using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var sr = new StreamReader(fs)) { var raw = sr.ReadToEnd(); var parts = raw.Split('|'); return parts.Length >= 1 ? parts[0] : null; } } catch { return null; } } private static bool IsStale(string path) { try { var age = DateTime.UtcNow - File.GetLastWriteTimeUtc(path); return age > StaleAfter; } catch { return false; } } public void Dispose() { try { _stream?.Dispose(); } catch { } _stream = null; if (!string.IsNullOrEmpty(_path)) { try { File.Delete(_path); } catch { } } } } }