Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31aef01ccd | |||
| 6c9721acc6 | |||
| 212ced4c81 | |||
| d3fa452504 | |||
| bee17a7fc6 |
@@ -36,8 +36,30 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
|
||||
wurde. Jede Seite hat jetzt eine eigene Baseline (`LastOutlookHash` /
|
||||
`LastStarfaceHash`); nur tatsaechlich geaenderte Kontakte werden geschrieben.
|
||||
|
||||
### Hinzugefuegt
|
||||
|
||||
- **Clientuebergreifende Sync-Sperre (Mehrplatz).** In den Einstellungen laesst
|
||||
sich ein gemeinsames Verzeichnis (Netzlaufwerk/UNC) hinterlegen. Synct ein
|
||||
Arbeitsplatz, legt er dort eine Lock-Datei an (atomar via `CreateNew`); andere
|
||||
Clients warten, bis sie frei ist (bis 2 Min, sonst wird der Lauf uebersprungen).
|
||||
Stuerzt ein Client ab, uebernimmt ein anderer die verwaiste Sperre. Ist kein
|
||||
Verzeichnis konfiguriert oder nicht erreichbar, wird ohne diese Sperre
|
||||
weitergearbeitet (der lokale Schutz auf dem PC bleibt).
|
||||
- **Feldweises 3-Wege-Merge bei Konflikten (bidirektional).** Wenn derselbe
|
||||
Kontakt zwischen zwei Syncs auf beiden Seiten geaendert wurde, bleiben jetzt
|
||||
Aenderungen an *unterschiedlichen* Feldern beide erhalten (z.B. einer aendert
|
||||
die Telefonnummer in Outlook, ein anderer die E-Mail in Starface). Nur wenn
|
||||
DASSELBE Feld auf beiden Seiten unterschiedlich geaendert wurde, greift die
|
||||
Vorrang-Regel (Outlook gewinnt). Dafuer wird im Mapping zusaetzlich ein
|
||||
Snapshot des letzten Sync-Stands je Seite gespeichert. Solche echten
|
||||
Feld-Konflikte werden dem Benutzer per Tray-Meldung angezeigt.
|
||||
|
||||
### Geaendert
|
||||
|
||||
- **Doppelte Syncs verhindert (lokal).** Der Schutz gegen gleichzeitig laufende
|
||||
Syncs (manuell + Auto-Sync-Timer) ist jetzt atomar (`Interlocked`) statt eines
|
||||
nicht-atomaren `volatile bool`, bei dem beide in einem Zeitfenster
|
||||
durchrutschen konnten.
|
||||
- **Ein-Richtungs-Modi sind jetzt echtes "Ersetzen".** Outlook->Starface macht
|
||||
das Starface-Adressbuch zu einer exakten Kopie von Outlook: Kontakte, die nur
|
||||
in Starface existieren (kein Pendant in Outlook), werden geloescht.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
||||
|
||||
#define MyAppName "Starface Outlook Sync"
|
||||
#define MyAppVersion "0.0.0.26"
|
||||
#define MyAppVersion "0.0.0.27"
|
||||
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
|
||||
#define MyAppURL "https://www.hacker-net.de"
|
||||
#define MyAppExeName "StarfaceOutlookSync.exe"
|
||||
|
||||
@@ -53,6 +53,13 @@ namespace StarfaceOutlookSync.Models
|
||||
public string LastOutlookHash { get; set; } = "";
|
||||
public string LastStarfaceHash { get; set; } = "";
|
||||
|
||||
// Snapshot des zuletzt synchronisierten Stands je Seite. Ermoeglicht ein
|
||||
// feldweises 3-Wege-Merge bei Konflikten (welche Seite hat welches Feld
|
||||
// geaendert), statt den ganzen Datensatz zu ueberschreiben. Null bei
|
||||
// Alt-Mappings -> dann Fallback auf ganz-ueberschreiben.
|
||||
public UnifiedContact LastOutlook { get; set; }
|
||||
public UnifiedContact LastStarface { get; set; }
|
||||
|
||||
// Alt-Feld (vor v0.0.0.24). Nur noch fuer Migration bestehender Mappings.
|
||||
public string LastSyncHash { get; set; } = "";
|
||||
}
|
||||
@@ -65,5 +72,23 @@ namespace StarfaceOutlookSync.Models
|
||||
public int Updated { get; set; }
|
||||
public int Errors { get; set; }
|
||||
public System.Collections.Generic.List<string> ErrorMessages { get; set; } = new System.Collections.Generic.List<string>();
|
||||
|
||||
// Echte Feld-Konflikte (dasselbe Feld auf beiden Seiten geaendert), die
|
||||
// ueber die Vorrang-Regel aufgeloest wurden. Fuer Benutzer-Hinweise.
|
||||
public System.Collections.Generic.List<FieldConflict> Conflicts { get; set; } = new System.Collections.Generic.List<FieldConflict>();
|
||||
}
|
||||
|
||||
public class FieldConflict
|
||||
{
|
||||
public string StarfaceId { get; set; } = "";
|
||||
public string ContactName { get; set; } = "";
|
||||
public string Field { get; set; } = "";
|
||||
public string OutlookValue { get; set; } = "";
|
||||
public string StarfaceValue { get; set; } = "";
|
||||
public string Winner { get; set; } = ""; // "Outlook" oder "Starface"
|
||||
|
||||
public override string ToString() =>
|
||||
$"{ContactName}: Feld '{Field}' auf beiden Seiten geaendert " +
|
||||
$"(Outlook: '{OutlookValue}' / Starface: '{StarfaceValue}') -> {Winner} uebernommen";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ namespace StarfaceOutlookSync.Models
|
||||
public bool SyncOnStart { get; set; } = false;
|
||||
public bool AutoAcceptOutlookPrompt { get; set; } = false;
|
||||
|
||||
// Gemeinsames Verzeichnis (Netzlaufwerk/UNC) fuer die clientuebergreifende
|
||||
// Sync-Sperre. Leer = keine Sperre (nur lokaler Schutz). Verhindert, dass
|
||||
// mehrere Arbeitsplaetze gleichzeitig dasselbe Adressbuch synchronisieren.
|
||||
public string SharedDirectory { get; set; } = "";
|
||||
|
||||
private static readonly string SettingsFile = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"StarfaceOutlookSync", "settings.json");
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StarfaceOutlookSync.Models;
|
||||
|
||||
namespace StarfaceOutlookSync.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Feldweises 3-Wege-Merge zweier Kontaktstaende gegen ihre Baseline.
|
||||
/// Erlaubt es, dass zwei Stellen unterschiedliche Felder desselben Kontakts
|
||||
/// aendern, ohne dass eine Aenderung verloren geht. Nur wenn DASSELBE Feld
|
||||
/// auf beiden Seiten geaendert wurde, greift die Vorrang-Regel.
|
||||
/// </summary>
|
||||
public static class ContactMerger
|
||||
{
|
||||
private class FieldDef
|
||||
{
|
||||
public string Label;
|
||||
public Func<UnifiedContact, string> Get;
|
||||
public Action<UnifiedContact, string> Set;
|
||||
public bool IsPhone;
|
||||
}
|
||||
|
||||
private static readonly FieldDef[] Fields = new[]
|
||||
{
|
||||
new FieldDef { Label = "Vorname", Get = c => c.FirstName, Set = (c, v) => c.FirstName = v },
|
||||
new FieldDef { Label = "Nachname", Get = c => c.LastName, Set = (c, v) => c.LastName = v },
|
||||
new FieldDef { Label = "Firma", Get = c => c.Company, Set = (c, v) => c.Company = v },
|
||||
new FieldDef { Label = "Position", Get = c => c.JobTitle, Set = (c, v) => c.JobTitle = v },
|
||||
new FieldDef { Label = "E-Mail", Get = c => c.Email, Set = (c, v) => c.Email = v },
|
||||
new FieldDef { Label = "E-Mail 2", Get = c => c.EmailSecondary, Set = (c, v) => c.EmailSecondary = v },
|
||||
new FieldDef { Label = "Telefon", Get = c => c.PhoneWork, Set = (c, v) => c.PhoneWork = v, IsPhone = true },
|
||||
new FieldDef { Label = "Mobil", Get = c => c.PhoneMobile, Set = (c, v) => c.PhoneMobile = v, IsPhone = true },
|
||||
new FieldDef { Label = "Telefon privat", Get = c => c.PhoneHome, Set = (c, v) => c.PhoneHome = v, IsPhone = true },
|
||||
new FieldDef { Label = "Fax", Get = c => c.Fax, Set = (c, v) => c.Fax = v, IsPhone = true },
|
||||
new FieldDef { Label = "Strasse", Get = c => c.Street, Set = (c, v) => c.Street = v },
|
||||
new FieldDef { Label = "Ort", Get = c => c.City, Set = (c, v) => c.City = v },
|
||||
new FieldDef { Label = "PLZ", Get = c => c.PostalCode, Set = (c, v) => c.PostalCode = v },
|
||||
new FieldDef { Label = "Bundesland", Get = c => c.State, Set = (c, v) => c.State = v },
|
||||
new FieldDef { Label = "Land", Get = c => c.Country, Set = (c, v) => c.Country = v },
|
||||
new FieldDef { Label = "Webseite", Get = c => c.Website, Set = (c, v) => c.Website = v },
|
||||
new FieldDef { Label = "Notizen", Get = c => c.Notes, Set = (c, v) => c.Notes = v },
|
||||
new FieldDef { Label = "Anrede", Get = c => c.Salutation, Set = (c, v) => c.Salutation = v },
|
||||
new FieldDef { Label = "Titel", Get = c => c.Title, Set = (c, v) => c.Title = v },
|
||||
new FieldDef { Label = "Geburtstag", Get = c => c.Birthday, Set = (c, v) => c.Birthday = v },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fuehrt den Outlook- und Starface-Stand gegen ihre jeweilige Baseline
|
||||
/// zusammen. outlookWins entscheidet bei echten Feld-Konflikten.
|
||||
/// </summary>
|
||||
public static (UnifiedContact merged, List<FieldConflict> conflicts) Merge(
|
||||
UnifiedContact baseOutlook, UnifiedContact baseStarface,
|
||||
UnifiedContact outlook, UnifiedContact starface,
|
||||
bool outlookWins)
|
||||
{
|
||||
var merged = new UnifiedContact
|
||||
{
|
||||
OutlookEntryId = outlook.OutlookEntryId,
|
||||
StarfaceId = starface.StarfaceId
|
||||
};
|
||||
var conflicts = new List<FieldConflict>();
|
||||
|
||||
foreach (var f in Fields)
|
||||
{
|
||||
string olv = f.Get(outlook) ?? "";
|
||||
string sfv = f.Get(starface) ?? "";
|
||||
string bol = f.Get(baseOutlook) ?? "";
|
||||
string bsf = f.Get(baseStarface) ?? "";
|
||||
|
||||
bool olChanged = !Equal(olv, bol, f.IsPhone);
|
||||
bool sfChanged = !Equal(sfv, bsf, f.IsPhone);
|
||||
|
||||
string chosen;
|
||||
if (olChanged && !sfChanged)
|
||||
{
|
||||
chosen = olv;
|
||||
}
|
||||
else if (sfChanged && !olChanged)
|
||||
{
|
||||
chosen = sfv;
|
||||
}
|
||||
else if (olChanged && sfChanged)
|
||||
{
|
||||
if (Equal(olv, sfv, f.IsPhone))
|
||||
{
|
||||
// Beide auf denselben Wert geaendert -> kein echter Konflikt.
|
||||
chosen = olv;
|
||||
}
|
||||
else
|
||||
{
|
||||
chosen = outlookWins ? olv : sfv;
|
||||
conflicts.Add(new FieldConflict
|
||||
{
|
||||
StarfaceId = starface.StarfaceId,
|
||||
ContactName = outlook.DisplayName,
|
||||
Field = f.Label,
|
||||
OutlookValue = olv,
|
||||
StarfaceValue = sfv,
|
||||
Winner = outlookWins ? "Outlook" : "Starface"
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keine Seite hat dieses Feld geaendert -> Outlook-Wert behalten.
|
||||
chosen = olv;
|
||||
}
|
||||
|
||||
f.Set(merged, chosen);
|
||||
}
|
||||
|
||||
return (merged, conflicts);
|
||||
}
|
||||
|
||||
private static bool Equal(string a, string b, bool isPhone)
|
||||
{
|
||||
if (isPhone)
|
||||
return NormalizePhone(a) == NormalizePhone(b);
|
||||
return string.Equals(a ?? "", b ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string NormalizePhone(string phone)
|
||||
{
|
||||
if (string.IsNullOrEmpty(phone)) return "";
|
||||
return new string(phone.Where(c => char.IsDigit(c) || c == '+').ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,20 @@ namespace StarfaceOutlookSync.Services
|
||||
|
||||
private void Log(string message) => OnProgress?.Invoke(message);
|
||||
|
||||
/// <summary>
|
||||
/// Setzt die Baseline eines Mappings auf den uebergebenen Stand beider
|
||||
/// Seiten (Snapshot + Hash). Der Snapshot wird fuer das Feld-Merge bei
|
||||
/// kuenftigen Konflikten gebraucht.
|
||||
/// </summary>
|
||||
private static void SetBaseline(SyncMapping m, UnifiedContact outlook, UnifiedContact starface)
|
||||
{
|
||||
m.LastOutlook = outlook;
|
||||
m.LastStarface = starface;
|
||||
m.LastOutlookHash = outlook?.GetHash() ?? "";
|
||||
m.LastStarfaceHash = starface?.GetHash() ?? "";
|
||||
m.LastSyncHash = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Findet einen passenden Kontakt in der Kandidatenliste.
|
||||
/// Strenges Matching: Felder die auf einer Seite gefuellt sind muessen
|
||||
@@ -282,9 +296,7 @@ namespace StarfaceOutlookSync.Services
|
||||
if (string.IsNullOrEmpty(mapping.LastOutlookHash) &&
|
||||
string.IsNullOrEmpty(mapping.LastStarfaceHash))
|
||||
{
|
||||
mapping.LastOutlookHash = olHash;
|
||||
mapping.LastStarfaceHash = sfHash;
|
||||
mapping.LastSyncHash = "";
|
||||
SetBaseline(mapping, oc, sc);
|
||||
newMappings.Add(mapping);
|
||||
continue;
|
||||
}
|
||||
@@ -292,14 +304,23 @@ namespace StarfaceOutlookSync.Services
|
||||
bool olChanged = olHash != mapping.LastOutlookHash;
|
||||
bool sfChanged = sfHash != mapping.LastStarfaceHash;
|
||||
|
||||
if (!olChanged && !sfChanged)
|
||||
{
|
||||
// Unveraendert. Snapshots aelterer Mappings (nur Hash)
|
||||
// nachtragen, damit kuenftige Konflikte gemergt werden koennen.
|
||||
if (mapping.LastOutlook == null || mapping.LastStarface == null)
|
||||
SetBaseline(mapping, oc, sc);
|
||||
newMappings.Add(mapping);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (olChanged && !sfChanged && (profile.SyncDirection == SyncDirection.Both || profile.SyncDirection == SyncDirection.OutlookToStarface))
|
||||
{
|
||||
// Outlook hat sich geaendert -> Starface updaten
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastOutlookHash = olHash;
|
||||
mapping.LastStarfaceHash = updated.GetHash();
|
||||
SetBaseline(mapping, oc, updated);
|
||||
result.Updated++;
|
||||
Log($" Aktualisiert (OL->SF): {oc.DisplayName}");
|
||||
}
|
||||
@@ -310,22 +331,54 @@ namespace StarfaceOutlookSync.Services
|
||||
var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastStarfaceHash = sfHash;
|
||||
mapping.LastOutlookHash = updated.GetHash();
|
||||
SetBaseline(mapping, updated, sc);
|
||||
result.Updated++;
|
||||
Log($" Aktualisiert (SF->OL): {sc.DisplayName}");
|
||||
}
|
||||
}
|
||||
else if (olChanged && sfChanged)
|
||||
{
|
||||
// Beide geaendert -> Konflikt, Outlook bevorzugt
|
||||
if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
|
||||
// Beide Seiten geaendert.
|
||||
if (profile.SyncDirection == SyncDirection.Both
|
||||
&& mapping.LastOutlook != null && mapping.LastStarface != null)
|
||||
{
|
||||
// Feldweises 3-Wege-Merge: unterschiedliche Felder
|
||||
// bleiben beide erhalten; nur bei gleichem Feld auf
|
||||
// beiden Seiten gewinnt Outlook.
|
||||
var (merged, conflicts) = ContactMerger.Merge(
|
||||
mapping.LastOutlook, mapping.LastStarface, oc, sc, outlookWins: true);
|
||||
|
||||
var updatedSf = await starface.UpdateContactAsync(mapping.StarfaceId, merged, profile.StarfaceAddressBook);
|
||||
var updatedOl = _outlookService.UpdateContact(mapping.OutlookEntryId, merged);
|
||||
|
||||
if (updatedSf != null || updatedOl != null)
|
||||
{
|
||||
if (updatedOl != null)
|
||||
{
|
||||
mapping.LastOutlook = updatedOl;
|
||||
mapping.LastOutlookHash = updatedOl.GetHash();
|
||||
}
|
||||
if (updatedSf != null)
|
||||
{
|
||||
mapping.LastStarface = updatedSf;
|
||||
mapping.LastStarfaceHash = updatedSf.GetHash();
|
||||
}
|
||||
mapping.LastSyncHash = "";
|
||||
result.Updated++;
|
||||
foreach (var cf in conflicts) result.Conflicts.Add(cf);
|
||||
Log(conflicts.Count > 0
|
||||
? $" Beidseitig geaendert, zusammengefuehrt ({conflicts.Count} Feld-Konflikt(e), Outlook gewinnt): {oc.DisplayName}"
|
||||
: $" Beidseitig geaendert, zusammengefuehrt: {oc.DisplayName}");
|
||||
}
|
||||
}
|
||||
else if (profile.SyncDirection != SyncDirection.StarfaceToOutlook)
|
||||
{
|
||||
// Fallback ohne Snapshot bzw. OutlookToStarface:
|
||||
// Outlook gewinnt komplett.
|
||||
var updated = await starface.UpdateContactAsync(mapping.StarfaceId, oc, profile.StarfaceAddressBook);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastOutlookHash = olHash;
|
||||
mapping.LastStarfaceHash = updated.GetHash();
|
||||
SetBaseline(mapping, oc, updated);
|
||||
result.Updated++;
|
||||
Log($" Konflikt (OL gewinnt): {oc.DisplayName}");
|
||||
}
|
||||
@@ -335,14 +388,12 @@ namespace StarfaceOutlookSync.Services
|
||||
var updated = _outlookService.UpdateContact(mapping.OutlookEntryId, sc);
|
||||
if (updated != null)
|
||||
{
|
||||
mapping.LastStarfaceHash = sfHash;
|
||||
mapping.LastOutlookHash = updated.GetHash();
|
||||
SetBaseline(mapping, updated, sc);
|
||||
result.Updated++;
|
||||
Log($" Konflikt (SF gewinnt): {sc.DisplayName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Beide unveraendert -> nichts tun
|
||||
}
|
||||
|
||||
newMappings.Add(mapping);
|
||||
@@ -382,6 +433,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = oc.OutlookEntryId,
|
||||
StarfaceId = match.StarfaceId,
|
||||
LastOutlook = oc,
|
||||
LastStarface = updated,
|
||||
LastOutlookHash = oc.GetHash(),
|
||||
LastStarfaceHash = updated.GetHash()
|
||||
});
|
||||
@@ -403,6 +456,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = oc.OutlookEntryId,
|
||||
StarfaceId = created.StarfaceId,
|
||||
LastOutlook = oc,
|
||||
LastStarface = created,
|
||||
LastOutlookHash = oc.GetHash(),
|
||||
LastStarfaceHash = created.GetHash()
|
||||
});
|
||||
@@ -458,6 +513,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = match.OutlookEntryId,
|
||||
StarfaceId = sc.StarfaceId,
|
||||
LastStarface = sc,
|
||||
LastOutlook = updated,
|
||||
LastStarfaceHash = sc.GetHash(),
|
||||
LastOutlookHash = updated.GetHash()
|
||||
});
|
||||
@@ -478,6 +535,8 @@ namespace StarfaceOutlookSync.Services
|
||||
ProfileId = profile.Id,
|
||||
OutlookEntryId = created.OutlookEntryId,
|
||||
StarfaceId = sc.StarfaceId,
|
||||
LastStarface = sc,
|
||||
LastOutlook = created,
|
||||
LastStarfaceHash = sc.GetHash(),
|
||||
LastOutlookHash = created.GetHash()
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
|
||||
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
|
||||
<Product>Starface Outlook Sync</Product>
|
||||
<Version>0.0.0.26</Version>
|
||||
<AssemblyVersion>0.0.0.26</AssemblyVersion>
|
||||
<FileVersion>0.0.0.26</FileVersion>
|
||||
<Version>0.0.0.27</Version>
|
||||
<AssemblyVersion>0.0.0.27</AssemblyVersion>
|
||||
<FileVersion>0.0.0.27</FileVersion>
|
||||
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
|
||||
<Copyright>Stefan Hacker - HackerSoft</Copyright>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
var lblVersion = new Label
|
||||
{
|
||||
Text = "Version 0.0.0.26",
|
||||
Text = "Version 0.0.0.27",
|
||||
Left = 0, Top = 56, Width = 340, Height = 20,
|
||||
TextAlign = ContentAlignment.MiddleCenter,
|
||||
ForeColor = Color.Gray
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using System.Windows.Forms;
|
||||
@@ -22,7 +23,9 @@ namespace StarfaceOutlookSync.UI
|
||||
private StatusStrip _statusBar;
|
||||
private ToolStripStatusLabel _statusLabel;
|
||||
private Timer _autoSyncTimer;
|
||||
private volatile bool _syncRunning = false;
|
||||
// 0 = frei, 1 = Sync laeuft. Per Interlocked atomar geschaltet, damit
|
||||
// ein manueller Sync und der Auto-Sync-Timer nicht gleichzeitig starten.
|
||||
private int _syncRunning = 0;
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
@@ -337,15 +340,25 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
private async Task RunSync(SyncProfile profile)
|
||||
{
|
||||
if (_syncRunning)
|
||||
// Atomar pruefen-und-setzen: verhindert, dass manueller Sync und
|
||||
// Auto-Sync-Timer gleichzeitig denselben/einen Sync starten.
|
||||
if (Interlocked.CompareExchange(ref _syncRunning, 1, 0) != 0)
|
||||
{
|
||||
SetStatus("Sync laeuft bereits, bitte warten...");
|
||||
return;
|
||||
}
|
||||
|
||||
_syncRunning = true;
|
||||
SyncLock crossLock = null;
|
||||
try
|
||||
{
|
||||
// Clientuebergreifende Sperre (falls gemeinsames Verzeichnis konfiguriert).
|
||||
crossLock = await AcquireCrossClientLock();
|
||||
if (crossLock == null)
|
||||
{
|
||||
SetStatus("Anderer Arbeitsplatz synchronisiert gerade - uebersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetStatus($"Synchronisiere '{profile.Name}'...");
|
||||
_trayIcon.ShowBalloonTip(2000, "Starface Sync",
|
||||
$"Synchronisiere '{profile.Name}'...", ToolTipIcon.Info);
|
||||
@@ -354,10 +367,22 @@ namespace StarfaceOutlookSync.UI
|
||||
|
||||
var msg = $"{profile.Name}: {result.Created} erstellt, {result.Updated} aktualisiert";
|
||||
if (result.Errors > 0) msg += $", {result.Errors} Fehler";
|
||||
if (result.Conflicts.Count > 0) msg += $", {result.Conflicts.Count} Konflikt(e)";
|
||||
|
||||
_trayIcon.ShowBalloonTip(3000, "Starface Sync", msg,
|
||||
result.Errors > 0 ? ToolTipIcon.Warning : ToolTipIcon.Info);
|
||||
|
||||
// Echte Feld-Konflikte gesondert melden, damit der Benutzer weiss,
|
||||
// dass ein Wert ueberschrieben wurde.
|
||||
if (result.Conflicts.Count > 0)
|
||||
{
|
||||
var detail = string.Join("\n", result.Conflicts.Take(5).Select(c => c.ToString()));
|
||||
if (result.Conflicts.Count > 5)
|
||||
detail += $"\n... und {result.Conflicts.Count - 5} weitere";
|
||||
_trayIcon.ShowBalloonTip(10000,
|
||||
$"Konflikt bei {result.Conflicts.Count} Kontakt(en)", detail, ToolTipIcon.Warning);
|
||||
}
|
||||
|
||||
SetStatus(msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -368,7 +393,49 @@ namespace StarfaceOutlookSync.UI
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncRunning = false;
|
||||
crossLock?.Dispose();
|
||||
Interlocked.Exchange(ref _syncRunning, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holt die clientuebergreifende Lock-Datei aus dem konfigurierten
|
||||
/// gemeinsamen Verzeichnis. Wartet bis zu 2 Minuten, falls ein anderer
|
||||
/// Arbeitsplatz gerade synct. Gibt null zurueck, wenn die Sperre nicht
|
||||
/// erlangt werden konnte (-> diesen Lauf ueberspringen).
|
||||
/// Ist kein Verzeichnis konfiguriert oder es ist nicht erreichbar, wird
|
||||
/// ohne clientuebergreifende Sperre fortgefahren (lokaler Schutz bleibt).
|
||||
/// </summary>
|
||||
private async Task<SyncLock> AcquireCrossClientLock()
|
||||
{
|
||||
var dir = UserSettings.Load().SharedDirectory;
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
return SyncLock.NoOp();
|
||||
|
||||
try
|
||||
{
|
||||
// Legt das Verzeichnis bei Bedarf an (idempotent). Funktioniert mit
|
||||
// UNC-Pfaden (\\server\freigabe\...) - kein Netzlaufwerk noetig.
|
||||
System.IO.Directory.CreateDirectory(dir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
SetStatus("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++;
|
||||
SetStatus($"Warte auf anderen Arbeitsplatz ({heldBy ?? "unbekannt"})... ({attempt})");
|
||||
await Task.Delay(3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace StarfaceOutlookSync.UI
|
||||
public class SettingsForm : Form
|
||||
{
|
||||
private CheckBox _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook;
|
||||
private TextBox _txtSharedDir;
|
||||
private Button _btnBrowseShared;
|
||||
private Button _btnSave, _btnCancel;
|
||||
private readonly UserSettings _settings;
|
||||
|
||||
@@ -54,35 +56,77 @@ namespace StarfaceOutlookSync.UI
|
||||
var lblHint = new Label
|
||||
{
|
||||
Text = hintText,
|
||||
Left = 38, Top = 102, Width = 310, Height = 36,
|
||||
Left = 38, Top = 102, Width = 320, Height = 36,
|
||||
ForeColor = UserSettings.IsOutlookSecurityLockedByPolicy() ? Color.OrangeRed : Color.Gray,
|
||||
Font = new Font("Segoe UI", 8)
|
||||
};
|
||||
|
||||
var lblShared = new Label
|
||||
{
|
||||
Text = "Gemeinsames Verzeichnis fuer Sync-Sperre (Mehrplatz, optional):",
|
||||
Left = 20, Top = 150, AutoSize = true
|
||||
};
|
||||
|
||||
_txtSharedDir = new TextBox
|
||||
{
|
||||
Left = 20, Top = 172, Width = 250,
|
||||
Text = _settings.SharedDirectory
|
||||
};
|
||||
|
||||
_btnBrowseShared = new Button
|
||||
{
|
||||
Text = "...", Left = 274, Top = 171, Width = 36, Height = 24
|
||||
};
|
||||
_btnBrowseShared.Click += (s, e) => BrowseSharedDir();
|
||||
|
||||
var lblSharedHint = new Label
|
||||
{
|
||||
Text = "Netzlaufwerk/UNC, das alle Arbeitsplaetze erreichen. Leer = keine\n" +
|
||||
"clientuebergreifende Sperre (nur Schutz auf diesem PC).",
|
||||
Left = 20, Top = 198, Width = 330, Height = 32,
|
||||
ForeColor = Color.Gray, Font = new Font("Segoe UI", 8)
|
||||
};
|
||||
|
||||
_btnSave = new Button
|
||||
{
|
||||
Text = "Speichern", Left = 95, Top = 170, Width = 85, Height = 28,
|
||||
Text = "Speichern", Left = 95, Top = 240, Width = 85, Height = 28,
|
||||
DialogResult = DialogResult.None
|
||||
};
|
||||
_btnSave.Click += (s, e) => Save();
|
||||
|
||||
_btnCancel = new Button
|
||||
{
|
||||
Text = "Abbrechen", Left = 189, Top = 170, Width = 85, Height = 28,
|
||||
Text = "Abbrechen", Left = 189, Top = 240, Width = 85, Height = 28,
|
||||
DialogResult = DialogResult.Cancel
|
||||
};
|
||||
|
||||
Size = new Size(380, 260);
|
||||
Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint, _btnSave, _btnCancel });
|
||||
Size = new Size(380, 330);
|
||||
Controls.AddRange(new Control[] { _chkStartMinimized, _chkSyncOnStart, _chkAutoAcceptOutlook, lblHint,
|
||||
lblShared, _txtSharedDir, _btnBrowseShared, lblSharedHint, _btnSave, _btnCancel });
|
||||
AcceptButton = _btnSave;
|
||||
CancelButton = _btnCancel;
|
||||
}
|
||||
|
||||
private void BrowseSharedDir()
|
||||
{
|
||||
using (var dlg = new FolderBrowserDialog())
|
||||
{
|
||||
dlg.Description = "Gemeinsames Verzeichnis fuer die Sync-Sperre waehlen";
|
||||
if (!string.IsNullOrWhiteSpace(_txtSharedDir.Text))
|
||||
{
|
||||
try { dlg.SelectedPath = _txtSharedDir.Text; } catch { }
|
||||
}
|
||||
if (dlg.ShowDialog(this) == DialogResult.OK)
|
||||
_txtSharedDir.Text = dlg.SelectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
_settings.StartMinimized = _chkStartMinimized.Checked;
|
||||
_settings.SyncOnStart = _chkSyncOnStart.Checked;
|
||||
_settings.AutoAcceptOutlookPrompt = _chkAutoAcceptOutlook.Checked;
|
||||
_settings.SharedDirectory = _txtSharedDir.Text.Trim();
|
||||
_settings.Save();
|
||||
DialogResult = DialogResult.OK;
|
||||
Close();
|
||||
|
||||
Reference in New Issue
Block a user