Compare commits

...
3 Commits
Author SHA1 Message Date
duffyduck ba0b96f16a Release v0.0.3.3 2026-08-17 14:16:51 +02:00
duffyduckandClaude Opus 4.8 e7a87fbdf1 Fix DedupeForm crash when closed during run (ObjectDisposedException)
Wurde das Dubletten-Fenster geschlossen, waehrend Analyse/Zusammenfuehrung
noch lief, griff die Hintergrund-Fortsetzung auf das entsorgte Textfeld zu
-> ObjectDisposedException im async-void Handler -> App-Absturz.

- AppendLog ist jetzt disposed-sicher (IsDisposed/Disposing-Check, BeginInvoke,
  ObjectDisposedException/InvalidOperationException abgefangen).
- OnFormClosing verhindert das Schliessen, solange _busy (Analyse/Merge laeuft),
  sodass keine Fortsetzung auf entsorgte Controls trifft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 14:16:23 +02:00
duffyduckandClaude Opus 4.8 0076d84062 Make dedupe robust: independent sides, wait for sync, report failures
Nach der Vorschau blieben Dubletten teils stehen. Ursachen abgesichert:

- Outlook- und Anlagen-Seite laufen jetzt in getrennten try-Bloecken. Vorher
  brach ein Fehler beim Laden/Verarbeiten der Anlage die ganze Methode ab,
  bevor die Outlook-Seite ueberhaupt bereinigt wurde.
- RunExclusiveAsync wartet bis zu 60s auf einen laufenden Sync, statt sofort
  abzubrechen (Auto-Sync feuert jede Minute -> haeufige Kollision).
- Fehlgeschlagene Loeschungen werden jetzt als Fehler gemeldet (vorher still
  verschluckt), plus Gruppen-Zaehler ins Log.
- DedupeForm zeigt einen deutlichen Hinweis (MessageBox), wenn wegen eines
  laufenden Syncs nicht ausgefuehrt wurde, und laesst einen erneuten Versuch zu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 14:04:49 +02:00
7 changed files with 124 additions and 66 deletions
+5 -2
View File
@@ -16,8 +16,11 @@ Versionsschema ist `x.x.x.x` (siehe `release.sh`).
Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die uebrigen Felder aus den Dubletten aufgefuellt werden (kein Datenverlust), die uebrigen
werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste
bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben
Sperre wie ein Sync (kein gleichzeitiger Lauf), nutzt dieselbe Kontakt- Sperre wie ein Sync (kein gleichzeitiger Lauf - wartet bei laufendem Sync bis
Erkennung wie der Sync und wird vollstaendig protokolliert. zu 60s statt abzubrechen), nutzt dieselbe Kontakt-Erkennung wie der Sync und
wird vollstaendig protokolliert. Outlook- und Anlagen-Seite werden unabhaengig
verarbeitet (eine streikende Anlage verhindert nicht die Outlook-Bereinigung);
fehlgeschlagene Loeschungen werden gemeldet.
### Behoben ### Behoben
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php) ; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync" #define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.3.2" #define MyAppVersion "0.0.3.3"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation" #define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de" #define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe" #define MyAppExeName "StarfaceOutlookSync.exe"
@@ -90,8 +90,55 @@ namespace StarfaceOutlookSync.Services
var deletedStarfaceIds = new HashSet<string>(); var deletedStarfaceIds = new HashSet<string>();
var deletedOutlookIds = new HashSet<string>(); var deletedOutlookIds = new HashSet<string>();
// --- Outlook (lokal, unabhaengig - laeuft auch wenn die Anlage streikt) ---
try
{
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath); var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
var groups = GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId));
Log($"Outlook: {groups.Count} Gruppe(n) mit Dubletten.");
foreach (var g in groups)
{
try
{
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
if (changed && _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) == null)
{
report.Errors++;
report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
continue;
}
foreach (var dup in g.Duplicates)
{
if (_outlookService.DeleteContact(dup.OutlookEntryId))
{
deletedOutlookIds.Add(dup.OutlookEntryId);
report.OutlookDeleted++;
Log($" Outlook: Dublette geloescht -> '{g.Survivor.DisplayName}'");
}
else
{
report.Errors++;
report.Messages.Add($"Outlook: Dublette von '{g.Survivor.DisplayName}' konnte nicht geloescht werden.");
}
}
report.OutlookMerged++;
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
}
}
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add("Outlook gesamt: " + ex.Message);
}
// --- Telefonanlage (unabhaengig) ---
try
{
using (var backend = ContactBackendFactory.Create(profile.Connection)) using (var backend = ContactBackendFactory.Create(profile.Connection))
{ {
backend.OnDebug += Log; backend.OnDebug += Log;
@@ -99,33 +146,31 @@ namespace StarfaceOutlookSync.Services
throw new Exception("Login an der Telefonanlage fehlgeschlagen."); throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook); var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
var groups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
// --- Telefonanlage --- Log($"Telefonanlage: {groups.Count} Gruppe(n) mit Dubletten.");
foreach (var g in GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId))) foreach (var g in groups)
{ {
try try
{ {
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates); bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
bool survivorOk = true; if (changed && await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) == null)
if (changed)
survivorOk = await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) != null;
if (!survivorOk)
{ {
// Gewinner konnte nicht ergaenzt werden -> Dubletten NICHT loeschen
// (sonst gingen die nur dort vorhandenen Felder verloren).
report.Errors++; report.Errors++;
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen."); report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
continue; continue;
} }
foreach (var dup in g.Duplicates) foreach (var dup in g.Duplicates)
{ {
if (await backend.DeleteContactAsync(dup.StarfaceId)) if (await backend.DeleteContactAsync(dup.StarfaceId))
{ {
deletedStarfaceIds.Add(dup.StarfaceId); deletedStarfaceIds.Add(dup.StarfaceId);
report.RemoteDeleted++; report.RemoteDeleted++;
Log($" Anlage: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'"); Log($" Anlage: Dublette geloescht -> '{g.Survivor.DisplayName}'");
}
else
{
report.Errors++;
report.Messages.Add($"Anlage: Dublette von '{g.Survivor.DisplayName}' konnte nicht geloescht werden.");
} }
} }
report.RemoteMerged++; report.RemoteMerged++;
@@ -139,40 +184,11 @@ namespace StarfaceOutlookSync.Services
await backend.LogoutAsync(); await backend.LogoutAsync();
} }
// --- Outlook ---
foreach (var g in GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId)))
{
try
{
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
bool survivorOk = true;
if (changed)
survivorOk = _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) != null;
if (!survivorOk)
{
report.Errors++;
report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen.");
continue;
}
foreach (var dup in g.Duplicates)
{
if (_outlookService.DeleteContact(dup.OutlookEntryId))
{
deletedOutlookIds.Add(dup.OutlookEntryId);
report.OutlookDeleted++;
Log($" Outlook: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'");
}
}
report.OutlookMerged++;
} }
catch (Exception ex) catch (Exception ex)
{ {
report.Errors++; report.Errors++;
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}"); report.Messages.Add("Telefonanlage gesamt: " + ex.Message);
}
} }
// Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen. // Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen.
@@ -123,11 +123,19 @@ namespace StarfaceOutlookSync.Services
/// </summary> /// </summary>
public async Task<bool> RunExclusiveAsync(Func<Task> action, Action<string> status) public async Task<bool> RunExclusiveAsync(Func<Task> action, Action<string> status)
{ {
if (Interlocked.CompareExchange(ref _running, 1, 0) != 0) // Auf einen gerade laufenden Sync warten (bis 60s), statt sofort
// abzubrechen - der Auto-Sync feuert ja jede Minute.
var guardDeadline = DateTime.UtcNow.AddSeconds(60);
while (Interlocked.CompareExchange(ref _running, 1, 0) != 0)
{ {
status?.Invoke("Es laeuft bereits ein Sync / eine Aktion - bitte warten."); if (DateTime.UtcNow >= guardDeadline)
{
status?.Invoke("Es laeuft bereits ein Sync - bitte spaeter erneut versuchen.");
return false; return false;
} }
status?.Invoke("Warte, bis der laufende Sync fertig ist...");
await Task.Delay(1000);
}
SyncLock crossLock = null; SyncLock crossLock = null;
var sharedDir = UserSettings.Load().SharedDirectory; var sharedDir = UserSettings.Load().SharedDirectory;
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle> <AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company> <Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product> <Product>Starface Outlook Sync</Product>
<Version>0.0.3.2</Version> <Version>0.0.3.3</Version>
<AssemblyVersion>0.0.3.2</AssemblyVersion> <AssemblyVersion>0.0.3.3</AssemblyVersion>
<FileVersion>0.0.3.2</FileVersion> <FileVersion>0.0.3.3</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description> <Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright> <Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label var lblVersion = new Label
{ {
Text = "Version 0.0.3.2", Text = "Version 0.0.3.3",
Left = 0, Top = 56, Width = 340, Height = 20, Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter, TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray ForeColor = Color.Gray
+33 -2
View File
@@ -89,9 +89,32 @@ namespace StarfaceOutlookSync.UI
private void AppendLog(string message) private void AppendLog(string message)
{ {
if (InvokeRequired) { Invoke(new Action(() => AppendLog(message))); return; } // Der Aufruf kann aus dem Hintergrund kommen, nachdem das Fenster
// bereits geschlossen/entsorgt wurde -> defensiv absichern.
if (IsDisposed || Disposing) return;
try
{
if (InvokeRequired) { BeginInvoke(new Action(() => AppendLog(message))); return; }
if (_txtLog == null || _txtLog.IsDisposed) return;
_txtLog.AppendText(message + "\r\n"); _txtLog.AppendText(message + "\r\n");
} }
catch (ObjectDisposedException) { }
catch (InvalidOperationException) { } // Handle noch nicht/nicht mehr da
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// Nicht schliessen, solange Analyse/Zusammenfuehrung laeuft - sonst
// greifen die Hintergrund-Fortsetzungen auf entsorgte Controls zu.
if (_busy)
{
e.Cancel = true;
_lblSummary.Text = "Bitte warten - der Vorgang laeuft noch...";
_lblSummary.ForeColor = Color.OrangeRed;
return;
}
base.OnFormClosing(e);
}
private async Task Analyze() private async Task Analyze()
{ {
@@ -160,7 +183,15 @@ namespace StarfaceOutlookSync.UI
if (!ran) if (!ran)
{ {
AppendLog("Abgebrochen (Sync/Aktion laeuft oder anderer Arbeitsplatz aktiv)."); AppendLog("Abgebrochen: gerade laeuft ein Sync (oder ein anderer Arbeitsplatz ist aktiv).");
_lblSummary.Text = "Nicht ausgefuehrt - es laeuft gerade ein Sync. Bitte kurz warten und erneut versuchen.";
_lblSummary.ForeColor = Color.OrangeRed;
MessageBox.Show(this,
"Die Zusammenfuehrung wurde NICHT ausgefuehrt, weil gerade ein Sync laeuft " +
"(oder ein anderer Arbeitsplatz aktiv ist).\n\nBitte kurz warten und erneut auf " +
"'Zusammenfuehren starten' klicken.",
"Dubletten", MessageBoxButtons.OK, MessageBoxIcon.Warning);
_btnStart.Enabled = true; // erneuter Versuch moeglich
} }
else if (report != null) else if (report != null)
{ {