Compare commits

...
5 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
duffyduck 6cc3103040 Release v0.0.3.2 2026-08-17 13:30:59 +02:00
duffyduckandClaude Opus 4.8 844094d5c5 Keep UI responsive during sync (run engine off the UI thread)
Beim Tray-/Auto-Sync lief die Engine auf dem UI-Thread -> Fenster und
Tray-Kontextmenue froren ein (Menuepunkte reagierten nicht).

- MainForm.RunSync fuehrt die Engine jetzt via Task.Run im Hintergrund aus
  (wie schon der manuelle Fenster-Sync).
- Balloon() marshalled Tray-Meldungen thread-sicher per BeginInvoke auf den
  UI-Thread (die Fortsetzung kann nach Task.Run aus einem Pool-Thread kommen).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 13:29:49 +02:00
8 changed files with 139 additions and 68 deletions
+10 -2
View File
@@ -16,11 +16,19 @@ 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
- **Oberflaeche friert waehrend des Syncs nicht mehr ein.** Beim Tray-/Auto-Sync
lief die Sync-Engine auf dem UI-Thread, wodurch Fenster und Tray-Kontextmenue
blockierten (Menuepunkte reagierten nicht). Die Engine laeuft jetzt im
Hintergrund (`Task.Run`), die Tray-Meldungen werden thread-sicher auf den
UI-Thread marshalled.
- **Dubletten bei Firmen-/Service-Eintraegen (nur Nummer + Firma).** Solche
Eintraege wurden bei jedem Sync neu angelegt statt verknuepft, weil der
Nummern-Abgleich feldgenau (geschaeftlich↔geschaeftlich) und formatabhaengig
+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.1"
#define MyAppVersion "0.0.3.3"
#define MyAppPublisher "HackerSoft - Hacker-Net Telekommunikation"
#define MyAppURL "https://www.hacker-net.de"
#define MyAppExeName "StarfaceOutlookSync.exe"
@@ -90,8 +90,55 @@ namespace StarfaceOutlookSync.Services
var deletedStarfaceIds = 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 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))
{
backend.OnDebug += Log;
@@ -99,33 +146,31 @@ namespace StarfaceOutlookSync.Services
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 groups = GroupDuplicates(remoteContacts, c => mappedStarface.Contains(c.StarfaceId));
Log($"Telefonanlage: {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 && await backend.UpdateContactAsync(g.Survivor.StarfaceId, g.Survivor, profile.AddressBook) == 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($"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 -> 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++;
@@ -139,40 +184,11 @@ namespace StarfaceOutlookSync.Services
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)
{
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.
@@ -123,11 +123,19 @@ 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.");
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;
var sharedDir = UserSettings.Load().SharedDirectory;
@@ -7,9 +7,9 @@
<AssemblyTitle>Starface Outlook Sync</AssemblyTitle>
<Company>HackerSoft - Hacker-Net Telekommunikation</Company>
<Product>Starface Outlook Sync</Product>
<Version>0.0.3.1</Version>
<AssemblyVersion>0.0.3.1</AssemblyVersion>
<FileVersion>0.0.3.1</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.1",
Text = "Version 0.0.3.3",
Left = 0, Top = 56, Width = 340, Height = 20,
TextAlign = ContentAlignment.MiddleCenter,
ForeColor = Color.Gray
+33 -2
View File
@@ -89,9 +89,32 @@ namespace StarfaceOutlookSync.UI
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");
}
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)
{
+10 -2
View File
@@ -358,9 +358,11 @@ namespace StarfaceOutlookSync.UI
{
// Lokaler Guard, Lock-Datei, Konflikt-Notizen und Protokoll laufen
// zentral im Coordinator (gemeinsam mit dem manuellen Sync-Pfad).
// WICHTIG: die Engine im Hintergrund laufen lassen, damit das
// Fenster und das Tray-Kontextmenue waehrend des Syncs reagieren.
var outcome = await _coordinator.RunAsync(
profile,
runEngine: () => _syncEngine.SyncProfileAsync(profile),
runEngine: () => Task.Run(() => _syncEngine.SyncProfileAsync(profile)),
status: SetStatus);
if (outcome.Skipped)
@@ -426,7 +428,13 @@ namespace StarfaceOutlookSync.UI
private void Balloon(int ms, string title, string text, ToolTipIcon icon, bool warn)
{
bool allow = warn ? _notifyWarn : _notifyGeneral;
if (allow) _trayIcon.ShowBalloonTip(ms, title, text, icon);
if (!allow) return;
if (InvokeRequired)
{
BeginInvoke(new Action(() => _trayIcon.ShowBalloonTip(ms, title, text, icon)));
return;
}
_trayIcon.ShowBalloonTip(ms, title, text, icon);
}
private void SetStatus(string text)