Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
30 changes: 24 additions & 6 deletions WeatherExtension.Tests/ResxCompletenessTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,33 @@
[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");

/// <summary>
/// Walks up from AppContext.BaseDirectory until it finds WeatherExtension/Properties.
/// This is more robust than a fixed relative path depth.
/// </summary>
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<string> ReadKeys(string resxPath)

Check warning on line 47 in WeatherExtension.Tests/ResxCompletenessTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Change return type of method 'ReadKeys' from 'System.Collections.Generic.IReadOnlyList<string>' to 'System.Collections.Generic.List<string>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)

Check warning on line 47 in WeatherExtension.Tests/ResxCompletenessTests.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Change return type of method 'ReadKeys' from 'System.Collections.Generic.IReadOnlyList<string>' to 'System.Collections.Generic.List<string>' for improved performance (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1859)
{
var doc = XDocument.Load(resxPath);
return doc.Root!
Expand Down
76 changes: 64 additions & 12 deletions WeatherExtension/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Shmuelie.WinRTServer;
using Shmuelie.WinRTServer.CsWinRT;
using System.Threading;
using BaldBeardedBuilder.WeatherExtension;

namespace WeatherExtension;

Expand All @@ -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<WeatherExtension, IExtension>(() => 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<WeatherExtension, IExtension>(() => 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
{
Expand Down
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.ar.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@
<data name="compass_sw" xml:space="preserve"><value>ج.غ</value></data>
<data name="compass_w" xml:space="preserve"><value>غ</value></data>
<data name="compass_nw" xml:space="preserve"><value>ش.غ</value></data>
<data name="bug_report_title" xml:space="preserve"><value>إرسال تقرير عن خطأ</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>حفظ السجلات على سطح المكتب</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>فتح GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## كيفية الإبلاغ عن خطأ

1. انقر على **حفظ السجلات على سطح المكتب** لجمع سجلات التشخيص
2. انقر على **فتح GitHub Issues** لإنشاء مشكلة جديدة
3. صِف المشكلة التي واجهتها
4. اسحب ملف ZIP وأفلته في نص المشكلة

سيتم حفظ ملف ZIP على سطح المكتب.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>تم حفظ السجلات على سطح المكتب</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Wien, Österreich</value></data><data name="search_hint_favorite_shortcut" xml:s
<data name="compass_sw" xml:space="preserve"><value>SW</value></data>
<data name="compass_w" xml:space="preserve"><value>W</value></data>
<data name="compass_nw" xml:space="preserve"><value>NW</value></data>
<data name="bug_report_title" xml:space="preserve"><value>Fehlerbericht einreichen</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Protokolle auf Desktop speichern</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>GitHub Issues öffnen</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 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.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Protokolle auf Desktop gespeichert</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.es.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Buenos Aires, Argentina</value></data><data name="search_hint_favorite_shortcut"
<data name="compass_sw" xml:space="preserve"><value>SO</value></data>
<data name="compass_w" xml:space="preserve"><value>O</value></data>
<data name="compass_nw" xml:space="preserve"><value>NO</value></data>
<data name="bug_report_title" xml:space="preserve"><value>Enviar un informe de error</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Guardar registros en el escritorio</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Abrir GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 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.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Registros guardados en el escritorio</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.fr.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Bruxelles, Belgique</value></data><data name="search_hint_favorite_shortcut" xml
<data name="compass_sw" xml:space="preserve"><value>SO</value></data>
<data name="compass_w" xml:space="preserve"><value>O</value></data>
<data name="compass_nw" xml:space="preserve"><value>NO</value></data>
<data name="bug_report_title" xml:space="preserve"><value>Soumettre un rapport de bug</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Enregistrer les journaux sur le bureau</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Ouvrir GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 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.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Journaux enregistrés sur le bureau</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.it.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Lugano, Svizzera</value></data><data name="search_hint_favorite_shortcut" xml:sp
<data name="compass_sw" xml:space="preserve"><value>SO</value></data>
<data name="compass_w" xml:space="preserve"><value>O</value></data>
<data name="compass_nw" xml:space="preserve"><value>NO</value></data>
<data name="bug_report_title" xml:space="preserve"><value>Invia una segnalazione di bug</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Salva log sul Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Apri GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 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.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Log salvati sul Desktop</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.ja.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@
<data name="compass_sw" xml:space="preserve"><value>南西</value></data>
<data name="compass_w" xml:space="preserve"><value>西</value></data>
<data name="compass_nw" xml:space="preserve"><value>北西</value></data>
<data name="bug_report_title" xml:space="preserve"><value>バグを報告する</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>ログをデスクトップに保存</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>GitHub Issues を開く</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## バグの報告方法

1. 下の **ログをデスクトップに保存** をクリックして診断ログを収集します
2. **GitHub Issues を開く** をクリックして新しい Issue を作成します
3. 発生した問題を説明してください
4. ZIP ファイルを Issue 本文にドラッグ アンド ドロップしてください

ZIP ファイルはデスクトップに保存されます。</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>ログをデスクトップに保存しました</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.ko.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@
<data name="compass_sw" xml:space="preserve"><value>남서</value></data>
<data name="compass_w" xml:space="preserve"><value>서</value></data>
<data name="compass_nw" xml:space="preserve"><value>북서</value></data>
<data name="bug_report_title" xml:space="preserve"><value>버그 신고</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>로그를 바탕화면에 저장</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>GitHub Issues 열기</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 버그 신고 방법

1. 아래의 **로그를 바탕화면에 저장**을 클릭하여 진단 로그를 수집합니다
2. **GitHub Issues 열기**를 클릭하여 새 Issue를 생성합니다
3. 발생한 문제를 설명합니다
4. ZIP 파일을 Issue 본문에 끌어다 놓습니다

ZIP 파일이 바탕화면에 저장됩니다.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>로그가 바탕화면에 저장됨</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.nl.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Brussel, België</value></data><data name="search_hint_favorite_shortcut" xml:sp
<data name="compass_sw" xml:space="preserve"><value>ZW</value></data>
<data name="compass_w" xml:space="preserve"><value>W</value></data>
<data name="compass_nw" xml:space="preserve"><value>NW</value></data>
<data name="bug_report_title" xml:space="preserve"><value>Een bugrapport indienen</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Logbestanden opslaan op bureaublad</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>GitHub Issues openen</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 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.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logbestanden opgeslagen op bureaublad</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.pl.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Berlin, Niemcy</value></data><data name="search_hint_favorite_shortcut" xml:spac
<data name="compass_sw" xml:space="preserve"><value>SW</value></data>
<data name="compass_w" xml:space="preserve"><value>W</value></data>
<data name="compass_nw" xml:space="preserve"><value>NW</value></data>
<data name="bug_report_title" xml:space="preserve"><value>Zgłoś błąd</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Zapisz logi na pulpicie</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Otwórz GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## 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.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logi zapisano na pulpicie</value></data>
</root>
Loading
Loading