212ced4c81
Verhindert, dass mehrere Arbeitsplaetze gleichzeitig dasselbe Starface- Adressbuch synchronisieren (Dubletten/Lost-Updates bei echter Ueberlappung). - Neues optionales Setting "Gemeinsames Verzeichnis" (UserSettings.SharedDirectory) in der Einstellungen-Maske inkl. Ordner-Browser. - SyncLock: atomare Lock-Datei (FileMode.CreateNew) im gemeinsamen Verzeichnis, waehrend des Syncs offen gehalten -> bei Absturz gibt das OS das Handle frei und ein anderer Client uebernimmt die verwaiste Datei (Stale-Erkennung 15 Min, Loeschen scheitert am offenen Handle eines lebenden Halters). - MainForm wartet vor dem Sync bis zu 2 Min auf eine freie Sperre, sonst wird der Lauf uebersprungen. Ohne/bei nicht erreichbarem Verzeichnis laeuft der Sync ohne diese Sperre weiter (lokaler Interlocked-Schutz bleibt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
121 lines
4.3 KiB
C#
121 lines
4.3 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace StarfaceOutlookSync.Services
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Sperre, die nichts haelt (wenn kein gemeinsames Verzeichnis konfiguriert/erreichbar ist).</summary>
|
|
public static SyncLock NoOp() => new SyncLock(null, null);
|
|
|
|
/// <summary>
|
|
/// Versucht die Sperre zu holen. Gibt null zurueck, wenn gerade ein
|
|
/// anderer (lebender) Client synct. heldBy enthaelt - sofern lesbar -
|
|
/// Infos zum aktuellen Halter.
|
|
/// </summary>
|
|
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 { }
|
|
}
|
|
}
|
|
}
|
|
}
|