diff --git a/RELEASING.md b/RELEASING.md index 38bf96a..e494779 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -110,3 +110,57 @@ The workflow automatically: | Store submission rejected | Check Partner Center dashboard for validation errors | | WinGet submission fails | Verify `WINGET_TOKEN` secret is set and not expired. For the first release, manually submit using `wingetcreate new` | | Build fails | The release workflow uses the same build process as CI — check for build errors in the Actions log | + +## Graceful Shutdown & Uninstall + +### Architecture + +The extension now supports graceful shutdown during uninstall without requiring Command Palette to close. This is implemented via: + +- **ShutdownCoordinator**: Listens on a named pipe (`\\.\pipe\WeatherExtension-Shutdown`) for external shutdown signals, with a fallback named event (`WeatherExtension-Shutdown`) +- **Program.cs**: Uses `WaitHandle.WaitAny()` to coordinate between normal Command Palette shutdown and external uninstall signals +- **Watchdog Timer**: A 5-second timeout ensures the process exits cleanly even if disposal hangs + +### How Uninstall Works + +1. Windows initiates uninstall +2. Uninstall script or tool sends shutdown signal via the named pipe +3. Extension process receives signal and gracefully disposes all resources +4. Process exits cleanly, allowing Windows to remove the package +5. No manual restart of Command Palette required + +### Signaling Shutdown Externally (PowerShell Example) + +```powershell +# Signal graceful shutdown via named pipe +$pipeName = "WeatherExtension-Shutdown" +$pipeClient = New-Object System.IO.Pipes.NamedPipeClientStream(".", $pipeName, "Out") +try { + $pipeClient.Connect(1000) + $sw = New-Object System.IO.StreamWriter($pipeClient) + $sw.WriteLine("SHUTDOWN") + $sw.Flush() +} catch { + Write-Host "Could not signal shutdown: $_" +} finally { + $pipeClient.Close() +} +``` + +Alternatively, use the named event for simpler but less robust signaling: + +```powershell +# Signal shutdown via named event +[System.Threading.EventWaitHandle]::new($false, "Global", "WeatherExtension-Shutdown").Set() +``` + +### Resource Cleanup During Shutdown + +The extension ensures all resources are properly disposed during shutdown: +- Timers are stopped +- HTTP client connections are closed +- Cancellation tokens are signalled +- Event handler subscriptions are unsubscribed +- No dangling handles or resources remain + +See `ShutdownCoordinator.cs` and disposal methods for implementation details. diff --git a/WeatherExtension.Tests/ResxCompletenessTests.cs b/WeatherExtension.Tests/ResxCompletenessTests.cs index 32f5fc1..d891d49 100644 --- a/WeatherExtension.Tests/ResxCompletenessTests.cs +++ b/WeatherExtension.Tests/ResxCompletenessTests.cs @@ -18,14 +18,32 @@ namespace Microsoft.CmdPal.Ext.Weather.UnitTests; [TestClass] public class ResxCompletenessTests { - // Resolve the Properties directory relative to the test output directory. - // Test output: WeatherExtension.Tests/bin/Debug/net8.0-windows10.0.22621.0/ - // Target: WeatherExtension/Properties/ - private static readonly string PropertiesDir = Path.GetFullPath( - Path.Combine(AppContext.BaseDirectory, "../../../..", "WeatherExtension", "Properties")); - + private static readonly string PropertiesDir = FindPropertiesDirectory(); private static readonly string BaselineFile = Path.Combine(PropertiesDir, "Resources.resx"); + /// + /// Walks up from AppContext.BaseDirectory until it finds WeatherExtension/Properties. + /// This is more robust than a fixed relative path depth. + /// + private static string FindPropertiesDirectory() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + + while (current != null) + { + var candidate = Path.Combine(current.FullName, "WeatherExtension", "Properties"); + if (Directory.Exists(candidate)) + { + return candidate; + } + + current = current.Parent; + } + + throw new DirectoryNotFoundException( + $"Could not find WeatherExtension/Properties directory by walking up from {AppContext.BaseDirectory}"); + } + private static IReadOnlyList ReadKeys(string resxPath) { var doc = XDocument.Load(resxPath); diff --git a/WeatherExtension/Program.cs b/WeatherExtension/Program.cs index 1cd5c15..7463c32 100644 --- a/WeatherExtension/Program.cs +++ b/WeatherExtension/Program.cs @@ -6,6 +6,7 @@ using Shmuelie.WinRTServer; using Shmuelie.WinRTServer.CsWinRT; using System.Threading; +using BaldBeardedBuilder.WeatherExtension; namespace WeatherExtension; @@ -19,19 +20,70 @@ public static void Main(string[] args) global::Shmuelie.WinRTServer.ComServer server = new(); ManualResetEvent extensionDisposedEvent = new(false); + ShutdownCoordinator? coordinator = null; - // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. - // This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object. - // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. - WeatherExtension extensionInstance = new(extensionDisposedEvent); - server.RegisterClass(() => extensionInstance); - server.Start(); - - // This will make the main thread wait until the event is signalled by the extension class. - // Since we have single instance of the extension object, we exit as soon as it is disposed. - extensionDisposedEvent.WaitOne(); - server.Stop(); - server.UnsafeDispose(); + try + { + // We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called. + // This makes sure that only one instance of WeatherExtension is alive, which is returned every time the host asks for the IExtension object. + // If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate. + WeatherExtension extensionInstance = new(extensionDisposedEvent); + server.RegisterClass(() => extensionInstance); + + // Set up graceful shutdown signaling from external processes (e.g., uninstaller) + coordinator = new ShutdownCoordinator(); + + server.Start(); + + // Wait for either normal disposal (Command Palette shutdown) or external shutdown signal (uninstall) + WaitHandle[] waitHandles = [extensionDisposedEvent, coordinator.ShutdownHandle]; + int signalledIndex = WaitHandle.WaitAny(waitHandles); + + // Log which shutdown path was taken + if (signalledIndex == 1) + { + WeatherLogger.LogToHost( + MessageState.Info, + "External shutdown signal received; initiating graceful shutdown"); + + // If external shutdown signal fired (not normal disposal), + // manually trigger disposal to clean up resources + extensionInstance.Dispose(); + } + + // Set up watchdog timer: if shutdown takes too long, force exit + using var watchdog = new Timer( + _ => + { + WeatherLogger.LogToHost( + MessageState.Error, + "Shutdown watchdog timeout; forcing process exit"); + Environment.Exit(1); + }, + state: null, + dueTime: TimeSpan.FromSeconds(5), + period: Timeout.InfiniteTimeSpan); + + // Wait for extension disposal to complete + extensionDisposedEvent.WaitOne(); + + // Cancel the watchdog timer since shutdown completed in time + watchdog.Change(Timeout.Infinite, Timeout.Infinite); + + server.Stop(); + } + catch (Exception ex) + { + WeatherLogger.LogToHost( + MessageState.Error, + $"Fatal error during shutdown coordination: {ex.Message}"); + } + finally + { + coordinator?.Dispose(); + extensionDisposedEvent.Dispose(); + server?.UnsafeDispose(); + } } else { diff --git a/WeatherExtension/Properties/Resources.ar.resx b/WeatherExtension/Properties/Resources.ar.resx index 9ee2196..7004633 100644 --- a/WeatherExtension/Properties/Resources.ar.resx +++ b/WeatherExtension/Properties/Resources.ar.resx @@ -120,4 +120,16 @@ ج.غ غ Ø´.غ + إرسال تقرير عن خطأ + حفظ السجلات على سطح المكتب + فتح GitHub Issues + ## كيفية الإبلاغ عن خطأ + +1. انقر على **حفظ السجلات على سطح المكتب** لجمع سجلات التشخيص +2. انقر على **فتح GitHub Issues** لإنشاء مشكلة جديدة +3. صِف المشكلة التي واجهتها +4. اسحب ملف ZIP وأفلته في نص المشكلة + +سيتم حفظ ملف ZIP على سطح المكتب. + تم حفظ السجلات على سطح المكتب \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.de.resx b/WeatherExtension/Properties/Resources.de.resx index 3778e3c..cd50ead 100644 --- a/WeatherExtension/Properties/Resources.de.resx +++ b/WeatherExtension/Properties/Resources.de.resx @@ -120,4 +120,16 @@ Wien, ÖsterreichSW W NW + Fehlerbericht einreichen + Protokolle auf Desktop speichern + GitHub Issues öffnen + ## So melden Sie einen Fehler + +1. Klicken Sie unten auf **Protokolle auf Desktop speichern**, um Diagnoseprotokolle zu sammeln +2. Klicken Sie auf **GitHub Issues öffnen**, um ein neues Issue zu erstellen +3. Beschreiben Sie das aufgetretene Problem +4. Ziehen Sie die ZIP-Datei in den Issue-Text + +Die ZIP-Datei wird auf Ihrem Desktop gespeichert. + Protokolle auf Desktop gespeichert \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.es.resx b/WeatherExtension/Properties/Resources.es.resx index 878b4ec..95db3de 100644 --- a/WeatherExtension/Properties/Resources.es.resx +++ b/WeatherExtension/Properties/Resources.es.resx @@ -120,4 +120,16 @@ Buenos Aires, ArgentinaSO O NO + Enviar un informe de error + Guardar registros en el escritorio + Abrir GitHub Issues + ## Cómo informar de un error + +1. Haz clic en **Guardar registros en el escritorio** para recopilar registros de diagnóstico +2. Haz clic en **Abrir GitHub Issues** para crear un nuevo issue +3. Describe el problema que encontraste +4. Arrastra y suelta el archivo ZIP en el cuerpo del issue + +El archivo ZIP se guardará en tu escritorio. + Registros guardados en el escritorio \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.fr.resx b/WeatherExtension/Properties/Resources.fr.resx index 1212b1d..10450d0 100644 --- a/WeatherExtension/Properties/Resources.fr.resx +++ b/WeatherExtension/Properties/Resources.fr.resx @@ -120,4 +120,16 @@ Bruxelles, BelgiqueSO O NO + Soumettre un rapport de bug + Enregistrer les journaux sur le bureau + Ouvrir GitHub Issues + ## Comment signaler un bug + +1. Cliquez sur **Enregistrer les journaux sur le bureau** pour collecter les journaux de diagnostic +2. Cliquez sur **Ouvrir GitHub Issues** pour créer un nouveau ticket +3. Décrivez le problème rencontré +4. Glissez-déposez le fichier ZIP dans le corps du ticket + +Le fichier ZIP sera enregistré sur votre bureau. + Journaux enregistrés sur le bureau \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.it.resx b/WeatherExtension/Properties/Resources.it.resx index 19ae1f8..99f744d 100644 --- a/WeatherExtension/Properties/Resources.it.resx +++ b/WeatherExtension/Properties/Resources.it.resx @@ -120,4 +120,16 @@ Lugano, SvizzeraSO O NO + Invia una segnalazione di bug + Salva log sul Desktop + Apri GitHub Issues + ## Come segnalare un bug + +1. Fai clic su **Salva log sul Desktop** per raccogliere i log di diagnostica +2. Fai clic su **Apri GitHub Issues** per creare una nuova segnalazione +3. Descrivi il problema riscontrato +4. Trascina il file ZIP nel corpo della segnalazione + +Il file ZIP verrà salvato sul Desktop. + Log salvati sul Desktop \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.ja.resx b/WeatherExtension/Properties/Resources.ja.resx index 731de91..9db90d5 100644 --- a/WeatherExtension/Properties/Resources.ja.resx +++ b/WeatherExtension/Properties/Resources.ja.resx @@ -120,4 +120,16 @@ 南西 西 北西 + バグを報告する + ログをデスクトップに保存 + GitHub Issues を開く + ## バグの報告方法 + +1. 下の **ログをデスクトップに保存** をクリックして診断ログを収集します +2. **GitHub Issues を開く** をクリックして新しい Issue を作成します +3. 発生した問題を説明してください +4. ZIP ファイルを Issue 本文にドラッグ アンド ドロップしてください + +ZIP ファイルはデスクトップに保存されます。 + ログをデスクトップに保存しました \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.ko.resx b/WeatherExtension/Properties/Resources.ko.resx index 27e6aab..d3131b0 100644 --- a/WeatherExtension/Properties/Resources.ko.resx +++ b/WeatherExtension/Properties/Resources.ko.resx @@ -120,4 +120,16 @@ 남서 서 북서 + 버그 신고 + 로그를 바탕화면에 저장 + GitHub Issues 열기 + ## 버그 신고 방법 + +1. 아래의 **로그를 바탕화면에 저장**을 클릭하여 진단 로그를 수집합니다 +2. **GitHub Issues 열기**를 클릭하여 새 Issue를 생성합니다 +3. 발생한 문제를 설명합니다 +4. ZIP 파일을 Issue 본문에 끌어다 놓습니다 + +ZIP 파일이 바탕화면에 저장됩니다. + 로그가 바탕화면에 저장됨 \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.nl.resx b/WeatherExtension/Properties/Resources.nl.resx index a24033f..61ed917 100644 --- a/WeatherExtension/Properties/Resources.nl.resx +++ b/WeatherExtension/Properties/Resources.nl.resx @@ -120,4 +120,16 @@ Brussel, BelgiëZW W NW + Een bugrapport indienen + Logbestanden opslaan op bureaublad + GitHub Issues openen + ## Hoe een bug te melden + +1. Klik hieronder op **Logbestanden opslaan op bureaublad** om diagnoselogbestanden te verzamelen +2. Klik op **GitHub Issues openen** om een nieuw issue aan te maken +3. Beschrijf het probleem dat u bent tegengekomen +4. Sleep het ZIP-bestand naar de body van het issue + +Het ZIP-bestand wordt op uw bureaublad opgeslagen. + Logbestanden opgeslagen op bureaublad \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.pl.resx b/WeatherExtension/Properties/Resources.pl.resx index 7183d01..af2fa49 100644 --- a/WeatherExtension/Properties/Resources.pl.resx +++ b/WeatherExtension/Properties/Resources.pl.resx @@ -120,4 +120,16 @@ Berlin, NiemcySW W NW + Zgłoś błąd + Zapisz logi na pulpicie + Otwórz GitHub Issues + ## Jak zgłosić błąd + +1. Kliknij poniżej **Zapisz logi na pulpicie**, aby zebrać logi diagnostyczne +2. Kliknij **Otwórz GitHub Issues**, aby utworzyć nowe zgłoszenie +3. Opisz napotkany problem +4. Przeciągnij i upuść plik ZIP do treści zgłoszenia + +Plik ZIP zostanie zapisany na pulpicie. + Logi zapisano na pulpicie \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.pt-BR.resx b/WeatherExtension/Properties/Resources.pt-BR.resx index 47749b2..d543b07 100644 --- a/WeatherExtension/Properties/Resources.pt-BR.resx +++ b/WeatherExtension/Properties/Resources.pt-BR.resx @@ -120,4 +120,16 @@ Lisboa, PortugalSO O NO + Enviar um relatório de bug + Salvar logs na área de trabalho + Abrir GitHub Issues + ## Como reportar um bug + +1. Clique em **Salvar logs na área de trabalho** para coletar os logs de diagnóstico +2. Clique em **Abrir GitHub Issues** para criar uma nova issue +3. Descreva o problema encontrado +4. Arraste e solte o arquivo ZIP no corpo da issue + +O arquivo ZIP será salvo na sua área de trabalho. + Logs salvos na área de trabalho \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.ru.resx b/WeatherExtension/Properties/Resources.ru.resx index fb4be3d..5bc5d4d 100644 --- a/WeatherExtension/Properties/Resources.ru.resx +++ b/WeatherExtension/Properties/Resources.ru.resx @@ -120,4 +120,16 @@ ЮЗ Ğ— СЗ + Отправить отчёт об ошибке + Сохранить журналы на рабочий стол + Открыть GitHub Issues + ## Как сообщить об ошибке + +1. Нажмите **Сохранить журналы на рабочий стол**, чтобы собрать диагностические журналы +2. Нажмите **Открыть GitHub Issues**, чтобы создать новый тикет +3. Опишите возникшую проблему +4. Перетащите ZIP-файл в текст тикета + +ZIP-файл будет сохранён на рабочем столе. + Журналы сохранены на рабочий стол \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.tr.resx b/WeatherExtension/Properties/Resources.tr.resx index 24e21a5..4ba6ae0 100644 --- a/WeatherExtension/Properties/Resources.tr.resx +++ b/WeatherExtension/Properties/Resources.tr.resx @@ -245,4 +245,16 @@ Berlin, AlmanyaGB B KB + Hata Bildirimi Gönder + Günlükleri Masaüstüne Kaydet + GitHub Issues'ı Aç + ## Hata nasıl bildirilir + +1. Tanılama günlüklerini toplamak için aşağıdaki **Günlükleri Masaüstüne Kaydet**'e tıklayın +2. Yeni bir issue oluşturmak için **GitHub Issues'ı Aç**'a tıklayın +3. Karşılaştığınız sorunu açıklayın +4. ZIP dosyasını issue gövdesine sürükleyip bırakın + +ZIP dosyası masaüstünüze kaydedilecektir. + Günlükler masaüstüne kaydedildi \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.zh-Hans.resx b/WeatherExtension/Properties/Resources.zh-Hans.resx index d4eabb7..0b52a30 100644 --- a/WeatherExtension/Properties/Resources.zh-Hans.resx +++ b/WeatherExtension/Properties/Resources.zh-Hans.resx @@ -120,4 +120,16 @@ 西南 西 西北 + 提交错误报告 + 将日志保存到桌面 + 打开 GitHub Issues + ## 如何报告错误 + +1. 点击下方的 **将日志保存到桌面** 来收集诊断日志 +2. 点击 **打开 GitHub Issues** 创建新 Issue +3. 描述您遇到的问题 +4. 将 ZIP 文件拖放到 Issue 正文中 + +ZIP 文件将保存到您的桌面。 + 日志已保存到桌面 \ No newline at end of file diff --git a/WeatherExtension/Properties/Resources.zh-Hant.resx b/WeatherExtension/Properties/Resources.zh-Hant.resx index e77f6b5..bf88c97 100644 --- a/WeatherExtension/Properties/Resources.zh-Hant.resx +++ b/WeatherExtension/Properties/Resources.zh-Hant.resx @@ -120,4 +120,16 @@ 西南 西 西北 + 提交錯誤報告 + 將日誌儲存到桌面 + 開啟 GitHub Issues + ## 如何回報錯誤 + +1. 點擊下方的 **將日誌儲存到桌面** 來收集診斷日誌 +2. 點擊 **開啟 GitHub Issues** 建立新 Issue +3. 描述您遇到的問題 +4. 將 ZIP 檔案拖放到 Issue 內文中 + +ZIP 檔案將儲存到您的桌面。 + 日誌已儲存到桌面 \ No newline at end of file diff --git a/WeatherExtension/ShutdownCoordinator.cs b/WeatherExtension/ShutdownCoordinator.cs new file mode 100644 index 0000000..10de002 --- /dev/null +++ b/WeatherExtension/ShutdownCoordinator.cs @@ -0,0 +1,175 @@ +// Copyright (c) Bald Bearded Builder LLC +// Bald Bearded Builder LLC licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO.Pipes; +using System.Threading; +using BaldBeardedBuilder.WeatherExtension; +using Microsoft.CommandPalette.Extensions; + +namespace WeatherExtension; + +/// +/// Coordinates graceful shutdown of the extension via named pipes or named events. +/// Allows external processes (e.g., uninstaller) to signal shutdown without +/// relying on Command Palette's disposal chain. +/// +internal sealed partial class ShutdownCoordinator : IDisposable +{ + private readonly ManualResetEvent _shutdownSignal = new(false); + private readonly CancellationTokenSource _listenerCts = new(); + private Thread? _listenerThread; + + /// + /// Gets a WaitHandle that signals when external shutdown is requested. + /// + public WaitHandle ShutdownHandle => _shutdownSignal; + + /// + /// Initializes the coordinator and starts listening for shutdown signals + /// via named pipe and named event. + /// + public ShutdownCoordinator() + { + _listenerThread = new Thread(ListenForShutdownAsync) + { + Name = "WeatherExtension-ShutdownListener", + IsBackground = true, + }; + + _listenerThread.Start(); + } + + private void ListenForShutdownAsync() + { + // Try the primary shutdown method: named pipe + if (TryNamedPipeShutdown()) + { + _shutdownSignal.Set(); + return; + } + + // Fall back to named event if named pipe fails + if (TryNamedEventShutdown()) + { + _shutdownSignal.Set(); + return; + } + + // If both fail, log the error but continue — graceful degradation + // allows the extension to keep running if shutdown signaling is unavailable. + } + + /// + /// Attempts to listen on a named pipe for shutdown signals. + /// Returns true if shutdown was signaled; false if the operation failed or was cancelled. + /// + private static bool TryNamedPipeShutdown() + { + try + { + string pipeName = "WeatherExtension-Shutdown"; + + using var pipeServer = new NamedPipeServerStream( + pipeName, + PipeDirection.In, + 1, + PipeTransmissionMode.Message, + PipeOptions.None); + + pipeServer.WaitForConnection(); + + // Any data on the pipe triggers shutdown + byte[] buffer = new byte[1]; + int bytesRead = pipeServer.Read(buffer, 0, 1); + + return bytesRead > 0; + } + catch (OperationCanceledException) + { + // Graceful cancellation during listener shutdown + return false; + } + catch (IOException ex) + { + // Pipe-related errors: already exists, access denied, etc. + WeatherLogger.LogToHost( + MessageState.Info, + $"Named pipe shutdown listener failed: {ex.Message}"); + return false; + } + catch (UnauthorizedAccessException ex) + { + // Permissions issue + WeatherLogger.LogToHost( + MessageState.Info, + $"Named pipe shutdown listener denied: {ex.Message}"); + return false; + } + catch (Exception ex) + { + // Unexpected error + WeatherLogger.LogToHost( + MessageState.Error, + $"Unexpected error in named pipe shutdown listener: {ex.Message}"); + return false; + } + } + + /// + /// Attempts to listen on a named event for shutdown signals. + /// Returns true if shutdown was signaled; false if the operation failed or was cancelled. + /// + private bool TryNamedEventShutdown() + { + try + { + string eventName = "WeatherExtension-Shutdown"; + + // Try to open an existing event (created by external process or previous instance) + // If it doesn't exist, this creates a new one with initial state "not set" + using var shutdownEvent = new EventWaitHandle( + false, + EventResetMode.AutoReset, + eventName, + out bool createdNew); + + // If we created this event, no one will signal it from outside. + // Wait anyway in case another process signals it later. + int index = WaitHandle.WaitAny(new[] { shutdownEvent, _listenerCts.Token.WaitHandle }); + return index == 0; // 0 means the event was signaled; 1 means cancellation + } + catch (UnauthorizedAccessException ex) + { + // Permissions issue (e.g., event exists but we can't access it) + WeatherLogger.LogToHost( + MessageState.Info, + $"Named event shutdown listener denied: {ex.Message}"); + return false; + } + catch (Exception ex) + { + // Unexpected error + WeatherLogger.LogToHost( + MessageState.Error, + $"Unexpected error in named event shutdown listener: {ex.Message}"); + return false; + } + } + + /// + /// Stops the listener thread and releases resources. + /// + public void Dispose() + { + _listenerCts.Cancel(); + _listenerCts.Dispose(); + + if (_listenerThread?.IsAlive == true) + { + _listenerThread.Join(timeout: TimeSpan.FromSeconds(2)); + } + + _shutdownSignal.Dispose(); + } +} diff --git a/WeatherExtension/WeatherExtension.cs b/WeatherExtension/WeatherExtension.cs index f68584c..898fe2d 100644 --- a/WeatherExtension/WeatherExtension.cs +++ b/WeatherExtension/WeatherExtension.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Threading; +using BaldBeardedBuilder.WeatherExtension; using Microsoft.CommandPalette.Extensions; namespace WeatherExtension; @@ -12,8 +13,9 @@ namespace WeatherExtension; public sealed partial class WeatherExtension : IExtension, IDisposable { private readonly ManualResetEvent _extensionDisposedEvent; - private readonly WeatherCommandsProvider _provider = new(); + private bool _isDisposed; + private readonly object _disposeLock = new(); public WeatherExtension(ManualResetEvent extensionDisposedEvent) { @@ -29,9 +31,42 @@ public WeatherExtension(ManualResetEvent extensionDisposedEvent) }; } + /// + /// Disposes the extension and signals completion. + /// Thread-safe and idempotent: can be called multiple times from different threads. + /// public void Dispose() { - _provider?.Dispose(); - this._extensionDisposedEvent.Set(); + lock (_disposeLock) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + } + + try + { + _provider?.Dispose(); + } + catch (Exception ex) + { + WeatherLogger.LogToHost( + MessageState.Error, + $"Error during provider disposal: {ex.Message}"); + } + + try + { + this._extensionDisposedEvent.Set(); + } + catch (Exception ex) + { + WeatherLogger.LogToHost( + MessageState.Error, + $"Error setting disposal event: {ex.Message}"); + } } }