Skip to content

feat: add auto updater #117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open

feat: add auto updater #117

wants to merge 9 commits into from

Conversation

deansheather
Copy link
Member

@deansheather deansheather commented Jun 4, 2025

Closes: #122

Adds an auto updater utility using NetSparkle.

Most of the UI matches the way it's done in NetSparkle for the built-in UIs (I referenced the Avalonia and WPF ones mostly), including most of the copy and implementation (incl. quirks). I did this in case we ever want to contribute this upstream. The only major difference is how I handle the release notes stuff.

For release notes, I use the <description> tag to generate HTML for the web view. There's a hardcoded """template""" in the source code and a copy of sindresorhus/github-markdown-css in the repo that gets included as an asset in the binary. This matches what we do on macOS but diverges from the way NetSparkle does release notes rendering using <sparkle:releaseNotesUrl> (or something like that).

  • Adds a button to the right click context menu on the tray icon for checking for updates
  • Adds custom UIs for all parts of the updater
  • Implements "action" notifications with a click handler into UserNotifier
  • Adds Serilog output to the debug output window in VS in DEBUG builds

TODO:

  • App cast generation script
  • Release pipeline modifications
  • Generate secret key and add to repo secrets
  • Publish initial 0.5.0 version manually
  • Set final security and update source related settings in NetSparkle

Follow up:

  • Integrate into the settings UI, add controls for whether it should automatically check for updates etc.

devenv_2vdCHXcF7M
explorer_eLh2FqvWI4

TrayIcon.OpenCommand = Tray_OpenCommand;
TrayIcon.CheckForUpdatesCommand = Tray_CheckForUpdatesCommand;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ideally we wouldn't include this if it's administratively disabled.

Copy link
Member Author

Choose a reason for hiding this comment

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

Clicking it will show a popup explaining that it's disabled by an administrator, which I think is good enough for now. I'll file a ticket to revisit this

Copy link
Member Author

Choose a reason for hiding this comment

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


bool IUIFactory.HideReleaseNotes { get; set; }
bool IUIFactory.HideSkipButton { get; set; }
bool IUIFactory.HideRemindMeLaterButton { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

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

why are these bools IUIFactor instead of just normal public bool?

Is there some reason we only want them accessed via the interface?

Copy link
Member Author

Choose a reason for hiding this comment

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

I just like the pattern, discovered it while looking at some of the NetSparkle code

# -outputAppCastSignaturePath <path>
param (
[Parameter(Mandatory = $true)]
[ValidatePattern("^v\d+\.\d+\.\d+$")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

if we are going to enforce that the tag follows this format, then why not just compute it from the version? Seems like a gnarly bug if they didn't match versions.

return;
}

var dispatcherQueue = ((App)Application.Current).TrayWindow?.DispatcherQueue;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think we want it to be the UserNotifier's business to figure out which thread the activation needs to run on. The Sparkle stuff needs to be on the UI, but other actions might not need to be.

Getting onto the correct thread should be the job of the callback itself.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that most (if not all) callbacks will want to run on the UI thread so they can e.g. spawn a window, update fields on the view model.

Either way we have to do the workaround to use the TrayWindow's DispatcherQueue in this case, so I think this avoids us having to have this workaround in multiple places in the future.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I still think it shouldn't be in UserNotifier but if you're going to insist, then the DispatcherQueue needs to be passed in at startup in some reasonably portable way. Doing it like this will make this impossible to unit test.

.AddText(message)
.AddArgument(CoderNotificationId, id)
.BuildNotification();
ActionHandlers[id] = action;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Doesn't this end up leaking handlers for every notification we send that doesn't get clicked?

I think we need to define some notification types, and allow a single handler per type. It could be dynamically registered at runtime, or just hard-coded handlers per type in App.xaml.cs (and then UserNotifier doesn't even need to worry about activations, just sending notifications).

Copy link
Member Author

Choose a reason for hiding this comment

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

That's fair, but I'd prefer this to be handled at the UserNotifier level then in App.xaml.cs. Ideally the IUserNotifier interface has a method called HandleActivation that takes a custom data type for ease of testing.

And I guess each handler would either be in a dictionary or registered once at startup.

$newAppCastSignaturePath = $newAppCastPath + ".signature"
& ./scripts/Update-AppCast.ps1 `
-tag "${{ github.ref_name }}" `
-version "${{ steps.version.outputs.VERSION }}" `
Copy link
Contributor

Choose a reason for hiding this comment

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

considering the update will fail if we don't "bump" the version number (ie. previous is lower than new) - do we want to check this here within the script?

//
// HACK: This isn't super robust, but the security risk is
// minor anyway. Malicious apps running as the user could
// likely override this setting by altering the memory of
Copy link
Contributor

Choose a reason for hiding this comment

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

One way to remove this risk is by always treating registry as a source of truth instead of relying on the in-memory value to block this.

@@ -32,7 +32,7 @@
<XamlUICommand
Label="Show Window"
Description="Show Window"
Command="{x:Bind OpenCommand}">
Command="{x:Bind OpenCommand, Mode=OneWay}">
Copy link
Contributor

Choose a reason for hiding this comment

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

The command shouldn't need a direction on the bind IIRC

<XamlUICommand
Label="Check for Updates"
Description="Check for Updates"
Command="{x:Bind CheckForUpdatesCommand, Mode=OneWay}">
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Command="{x:Bind CheckForUpdatesCommand, Mode=OneWay}">
Command="{x:Bind CheckForUpdatesCommand}">

<MenuFlyoutItem>
<MenuFlyoutItem.Command>
<XamlUICommand
Label="Exit"
Description="Exit"
Command="{x:Bind ExitCommand}">
Command="{x:Bind ExitCommand, Mode=OneWay}">
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Command="{x:Bind ExitCommand, Mode=OneWay}">
Command="{x:Bind ExitCommand}">


InitializeComponent();
TitleBarIcon.SetTitlebarIcon(this);
SystemBackdrop = new DesktopAcrylicBackdrop();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
SystemBackdrop = new DesktopAcrylicBackdrop();


public UpdaterDownloadProgressViewModel ViewModel;

private bool _didCallDownloadProcessCompletedHandler;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private bool _didCallDownloadProcessCompletedHandler;
private bool _downloadProcessCompletedInvoked;

just a nitpick
having an auxilary verb in the property name sounds weird imho

MinWidth="600" MinHeight="500">

<Window.SystemBackdrop>
<DesktopAcrylicBackdrop />
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<DesktopAcrylicBackdrop />
<MicaBackdrop />


InitializeComponent();
TitleBarIcon.SetTitlebarIcon(this);
SystemBackdrop = new DesktopAcrylicBackdrop();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
SystemBackdrop = new DesktopAcrylicBackdrop();


void IUpdateAvailable.Close()
{
UserRespondedToUpdateCheck(UpdateAvailableResult.None); // the Avalonia UI does this "just in case"
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we reference the Avalonia UI in the comment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Coder Desktop Windows auto update
3 participants