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
werden geloescht. Verwaiste Zuordnungen werden bereinigt, sodass der naechste
bidirektionale Sync die Gewinner sauber 1:1 verbindet. Laeuft unter derselben
Sperre wie ein Sync (kein gleichzeitiger Lauf), nutzt dieselbe Kontakt-
Erkennung wie der Sync und wird vollstaendig protokolliert.
Sperre wie ein Sync (kein gleichzeitiger Lauf - wartet bei laufendem Sync bis
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
+1 -1
View File
@@ -2,7 +2,7 @@
; Erfordert Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
#define MyAppName "Starface Outlook Sync"
#define MyAppVersion "0.0.3.2"
#define MyAppVersion "0.0.3.3"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe"
@@ -90,90 +90,106 @@ namespace StarfaceOutlookSync.Services
var deletedStarfaceIds = new HashSet<string>();
var deletedOutlookIds = new HashSet<string>();
var outlookContacts = _outlookService.GetContacts(profile.OutlookFolderPath);
using (var backend = ContactBackendFactory.Create(profile.Connection))
// --- Outlook (lokal, unabhaengig - laeuft auch wenn die Anlage streikt) ---
try
{
backend.OnDebug += Log;
if (!await backend.LoginAsync())
throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
// --- Telefonanlage ---
foreach (var g in GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId)))
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);
bool survivorOk = true;
if (changed)
survivorOk = await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) != null;
if (!survivorOk)
if (changed && _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) == null)
{
// Gewinner konnte nicht ergaenzt werden -> Dubletten NICHT loeschen
// (sonst gingen die nur dort vorhandenen Felder verloren).
report.Errors++;
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - Gruppe uebersprungen.");
report.Messages.Add($"Outlook: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
continue;
}
foreach (var dup in g.Duplicates)
{
if (await backend.DeleteContactAsync(dup.StarfaceId))
if (_outlookService.DeleteContact(dup.OutlookEntryId))
{
deletedStarfaceIds.Add(dup.StarfaceId);
report.RemoteDeleted++;
Log($" Anlage: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'");
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.RemoteMerged++;
report.OutlookMerged++;
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add($"Anlage '{g.Survivor.DisplayName}': {ex.Message}");
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
}
}
await backend.LogoutAsync();
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add("Outlook gesamt: " + ex.Message);
}
// --- Outlook ---
foreach (var g in GroupDuplicates(outlookContacts, c => mappedOutlook.Contains(c.OutlookEntryId)))
// --- Telefonanlage (unabhaengig) ---
try
{
try
using (var backend = ContactBackendFactory.Create(profile.Connection))
{
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
bool survivorOk = true;
if (changed)
survivorOk = _outlookService.UpdateContact(g.Survivor.OutlookEntryId, g.Survivor) != null;
backend.OnDebug += Log;
if (!await backend.LoginAsync())
throw new Exception("Login an der Telefonanlage fehlgeschlagen.");
if (!survivorOk)
var remoteContacts = await backend.GetContactsAsync(profile.AddressBook);
var groups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
Log($"Telefonanlage: {groups.Count} Gruppe(n) mit Dubletten.");
foreach (var g in groups)
{
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))
try
{
deletedOutlookIds.Add(dup.OutlookEntryId);
report.OutlookDeleted++;
Log($" Outlook: Dublette geloescht -> zusammengefuehrt in '{g.Survivor.DisplayName}'");
bool changed = ContactMerger.FillEmptyInto(g.Survivor, g.Duplicates);
if (changed && await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) == null)
{
report.Errors++;
report.Messages.Add($"Anlage: '{g.Survivor.DisplayName}' konnte nicht ergaenzt werden - uebersprungen.");
continue;
}
foreach (var dup in g.Duplicates)
{
if (await backend.DeleteContactAsync(dup.StarfaceId))
{
deletedStarfaceIds.Add(dup.StarfaceId);
report.RemoteDeleted++;
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++;
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add($"Anlage '{g.Survivor.DisplayName}': {ex.Message}");
}
}
report.OutlookMerged++;
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add($"Outlook '{g.Survivor.DisplayName}': {ex.Message}");
await backend.LogoutAsync();
}
}
catch (Exception ex)
{
report.Errors++;
report.Messages.Add("Telefonanlage gesamt: " + ex.Message);
}
// Verwaiste Zuordnungen (zeigen auf geloeschte Dubletten) entfernen.
// Der naechste Sync verbindet die Gewinner sauber neu.
@@ -123,10 +123,18 @@ namespace StarfaceOutlookSync.Services
/// </summary>
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.");
return false;
if (DateTime.UtcNow >= guardDeadline)
{
status?.Invoke("Es laeuft bereits ein Sync - bitte spaeter erneut versuchen.");
return false;
}
status?.Invoke("Warte, bis der laufende Sync fertig ist...");
await Task.Delay(1000);
}
SyncLock crossLock = null;
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product>
<Version>0.0.3.2</Version>
<AssemblyVersion>0.0.3.2</AssemblyVersion>
<FileVersion>0.0.3.2</FileVersion>
<Version>0.0.3.3</Version>
<AssemblyVersion>0.0.3.3</AssemblyVersion>
<FileVersion>0.0.3.3</FileVersion>
<Description>Synchronisiert Outlook-Kontakte mit Starface Telefonanlage</Description>
<Copyright>Stefan Hacker - HackerSoft</Copyright>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StarfaceOutlookSync.UI
var lblVersion = new Label
{
Text = "Version 0.0.3.2",
Text = "Version 0.0.3.3",
Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray
+34 -3
View File
@@ -89,8 +89,31 @@ namespace StarfaceOutlookSync.UI
private void AppendLog(string message)
{
if (InvokeRequired) { Invoke(new Action(() => AppendLog(message))); return; }
_txtLog.AppendText(message + "\r\n");
// 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");
}
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()
@@ -160,7 +183,15 @@ namespace StarfaceOutlookSync.UI
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)
{