Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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)
{
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 SampleExtension is alive, which is returned every time the host asks for the IExtension object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not SampleExtension

// 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>Submit a Bug Report</value></data>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't seem to be translating for the appropriate resource files. These all need their language rather than English

<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to 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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</value></data>
</root>
12 changes: 12 additions & 0 deletions WeatherExtension/Properties/Resources.pt-BR.resx
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ Lisboa, Portugal</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>Submit a Bug Report</value></data>
<data name="bug_report_save_logs" xml:space="preserve"><value>Save Logs to Desktop</value></data>
<data name="bug_report_open_github" xml:space="preserve"><value>Open GitHub Issues</value></data>
<data name="bug_report_instructions" xml:space="preserve"><value>## How to report a bug

1. Click **Save Logs to Desktop** below to collect diagnostic logs
2. Click **Open GitHub Issues** to create a new issue
3. Describe the problem you encountered
4. Drag and drop the log zip file into the issue body

The zip file will be saved to your Desktop.</value></data>
<data name="bug_report_logs_saved" xml:space="preserve"><value>Logs saved to Desktop</value></data>
</root>
Loading
Loading