Skip to content

Commit 46849a5

Browse files
authored
feat: add auto updater (#117)
1 parent 56003ed commit 46849a5

38 files changed

+2960
-65
lines changed

.github/workflows/release.yaml

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ jobs:
6868
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
6969
token_format: "access_token"
7070

71+
- name: Install gcloud
72+
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # 2.1.4
73+
7174
- name: Install wix
7275
shell: pwsh
7376
run: |
@@ -120,6 +123,51 @@ jobs:
120123
env:
121124
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
122125

126+
- name: Update appcast
127+
if: startsWith(github.ref, 'refs/tags/')
128+
shell: pwsh
129+
run: |
130+
$ErrorActionPreference = "Stop"
131+
132+
# The Update-AppCast.ps1 script fetches the release notes from GitHub,
133+
# which might take a few seconds to be ready.
134+
Start-Sleep -Seconds 10
135+
136+
# Save the appcast signing key to a temporary file.
137+
$keyPath = Join-Path $env:TEMP "appcast-key.pem"
138+
$key = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:APPCAST_SIGNATURE_KEY_BASE64))
139+
Set-Content -Path $keyPath -Value $key
140+
141+
# Download the old appcast from GCS.
142+
$oldAppCastPath = Join-Path $env:TEMP "appcast.old.xml"
143+
& gsutil cp $env:APPCAST_GCS_URI $oldAppCastPath
144+
if ($LASTEXITCODE -ne 0) { throw "Failed to download appcast" }
145+
146+
# Generate the new appcast and signature.
147+
$newAppCastPath = Join-Path $env:TEMP "appcast.new.xml"
148+
$newAppCastSignaturePath = $newAppCastPath + ".signature"
149+
& ./scripts/Update-AppCast.ps1 `
150+
-tag "${{ github.ref_name }}" `
151+
-channel stable `
152+
-x64Path "${{ steps.release.outputs.X64_OUTPUT_PATH }}" `
153+
-arm64Path "${{ steps.release.outputs.ARM64_OUTPUT_PATH }}" `
154+
-keyPath $keyPath `
155+
-inputAppCastPath $oldAppCastPath `
156+
-outputAppCastPath $newAppCastPath `
157+
-outputAppCastSignaturePath $newAppCastSignaturePath
158+
if ($LASTEXITCODE -ne 0) { throw "Failed to generate new appcast" }
159+
160+
# Upload the new appcast and signature to GCS.
161+
& gsutil -h "Cache-Control:no-cache,max-age=0" cp $newAppCastPath $env:APPCAST_GCS_URI
162+
if ($LASTEXITCODE -ne 0) { throw "Failed to upload new appcast" }
163+
& gsutil -h "Cache-Control:no-cache,max-age=0" cp $newAppCastSignaturePath $env:APPCAST_SIGNATURE_GCS_URI
164+
if ($LASTEXITCODE -ne 0) { throw "Failed to upload new appcast signature" }
165+
env:
166+
APPCAST_GCS_URI: gs://releases.coder.com/coder-desktop/windows/appcast.xml
167+
APPCAST_SIGNATURE_GCS_URI: gs://releases.coder.com/coder-desktop/windows/appcast.xml.signature
168+
APPCAST_SIGNATURE_KEY_BASE64: ${{ secrets.APPCAST_SIGNATURE_KEY_BASE64 }}
169+
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
170+
123171
winget:
124172
runs-on: depot-windows-latest
125173
needs: release
@@ -177,7 +225,6 @@ jobs:
177225
# to GitHub and then making a PR in a different repo.
178226
WINGET_GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
179227

180-
181228
- name: Comment on PR
182229
run: |
183230
# wait 30 seconds

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,12 @@ publish
411411
*.wixmdb
412412
*.wixprj
413413
*.wixproj
414+
415+
appcast.xml
416+
appcast.xml.signature
417+
*.key
418+
*.key.*
419+
*.pem
420+
*.pem.*
421+
*.pub
422+
*.pub.*

App/App.csproj

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
<DefineConstants>DISABLE_XAML_GENERATED_MAIN;DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION</DefineConstants>
2020

2121
<AssemblyName>Coder Desktop</AssemblyName>
22+
<AssemblyTitle>Coder Desktop</AssemblyTitle>
23+
<Company>Coder Technologies Inc.</Company>
24+
<Product>Coder Desktop</Product>
25+
<Copyright>© Coder Technologies Inc.</Copyright>
2226
<ApplicationIcon>coder.ico</ApplicationIcon>
2327
</PropertyGroup>
2428

@@ -31,9 +35,7 @@
3135

3236
<ItemGroup>
3337
<Content Include="coder.ico" />
34-
</ItemGroup>
35-
36-
<ItemGroup>
38+
<EmbeddedResource Include="Assets\changelog.css" />
3739
<Manifest Include="$(ApplicationManifest)" />
3840
</ItemGroup>
3941

@@ -68,12 +70,17 @@
6870
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
6971
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.4" />
7072
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
73+
<PackageReference Include="NetSparkleUpdater.SparkleUpdater" Version="3.0.2" />
7174
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
7275
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
7376
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
7477
<PackageReference Include="WinUIEx" Version="2.5.1" />
7578
</ItemGroup>
7679

80+
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
81+
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
82+
</ItemGroup>
83+
7784
<ItemGroup>
7885
<ProjectReference Include="..\CoderSdk\CoderSdk.csproj" />
7986
<ProjectReference Include="..\MutagenSdk\MutagenSdk.csproj" />

App/App.xaml.cs

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,46 @@
1616
using Microsoft.Extensions.DependencyInjection;
1717
using Microsoft.Extensions.Hosting;
1818
using Microsoft.Extensions.Logging;
19+
using Microsoft.UI.Dispatching;
1920
using Microsoft.UI.Xaml;
2021
using Microsoft.Win32;
2122
using Microsoft.Windows.AppLifecycle;
2223
using Microsoft.Windows.AppNotifications;
24+
using NetSparkleUpdater.Interfaces;
2325
using Serilog;
2426
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs;
2527

2628
namespace Coder.Desktop.App;
2729

28-
public partial class App : Application
30+
public partial class App : Application, IDispatcherQueueManager
2931
{
30-
private readonly IServiceProvider _services;
31-
32-
private bool _handleWindowClosed = true;
3332
private const string MutagenControllerConfigSection = "MutagenController";
33+
private const string UpdaterConfigSection = "Updater";
3434

3535
#if !DEBUG
3636
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\App";
37-
private const string logFilename = "app.log";
37+
private const string LogFilename = "app.log";
38+
private const string DefaultLogLevel = "Information";
3839
#else
3940
private const string ConfigSubKey = @"SOFTWARE\Coder Desktop\DebugApp";
40-
private const string logFilename = "debug-app.log";
41+
private const string LogFilename = "debug-app.log";
42+
private const string DefaultLogLevel = "Debug";
4143
#endif
4244

45+
// HACK: This is exposed for dispatcher queue access. The notifier uses
46+
// this to ensure action callbacks run in the UI thread (as
47+
// activation events aren't in the main thread).
48+
public TrayWindow? TrayWindow;
49+
50+
private readonly IServiceProvider _services;
4351
private readonly ILogger<App> _logger;
4452
private readonly IUriHandler _uriHandler;
45-
53+
private readonly IUserNotifier _userNotifier;
4654
private readonly ISettingsManager<CoderConnectSettings> _settingsManager;
47-
4855
private readonly IHostApplicationLifetime _appLifetime;
4956

57+
private bool _handleWindowClosed = true;
58+
5059
public App()
5160
{
5261
var builder = Host.CreateApplicationBuilder();
@@ -58,7 +67,17 @@ public App()
5867
configBuilder.Add(
5968
new RegistryConfigurationSource(Registry.LocalMachine, ConfigSubKey));
6069
configBuilder.Add(
61-
new RegistryConfigurationSource(Registry.CurrentUser, ConfigSubKey));
70+
new RegistryConfigurationSource(
71+
Registry.CurrentUser,
72+
ConfigSubKey,
73+
// Block "Updater:" configuration from HKCU, so that updater
74+
// settings can only be set at the HKLM level.
75+
//
76+
// HACK: This isn't super robust, but the security risk is
77+
// minor anyway. Malicious apps running as the user could
78+
// likely override this setting by altering the memory of
79+
// this app.
80+
UpdaterConfigSection + ":"));
6281

6382
var services = builder.Services;
6483

@@ -71,6 +90,7 @@ public App()
7190
services.AddSingleton<ICoderApiClientFactory, CoderApiClientFactory>();
7291
services.AddSingleton<IAgentApiClientFactory, AgentApiClientFactory>();
7392

93+
services.AddSingleton<IDispatcherQueueManager>(_ => this);
7494
services.AddSingleton<ICredentialBackend>(_ =>
7595
new WindowsCredentialBackend(WindowsCredentialBackend.CoderCredentialsTargetName));
7696
services.AddSingleton<ICredentialManager, CredentialManager>();
@@ -84,6 +104,12 @@ public App()
84104
services.AddSingleton<IRdpConnector, RdpConnector>();
85105
services.AddSingleton<IUriHandler, UriHandler>();
86106

107+
services.AddOptions<UpdaterConfig>()
108+
.Bind(builder.Configuration.GetSection(UpdaterConfigSection));
109+
services.AddSingleton<IUpdaterUpdateAvailableViewModelFactory, UpdaterUpdateAvailableViewModelFactory>();
110+
services.AddSingleton<IUIFactory, CoderSparkleUIFactory>();
111+
services.AddSingleton<IUpdateController, SparkleUpdateController>();
112+
87113
// SignInWindow views and view models
88114
services.AddTransient<SignInViewModel>();
89115
services.AddTransient<SignInWindow>();
@@ -119,6 +145,7 @@ public App()
119145
_services = services.BuildServiceProvider();
120146
_logger = _services.GetRequiredService<ILogger<App>>();
121147
_uriHandler = _services.GetRequiredService<IUriHandler>();
148+
_userNotifier = _services.GetRequiredService<IUserNotifier>();
122149
_settingsManager = _services.GetRequiredService<ISettingsManager<CoderConnectSettings>>();
123150
_appLifetime = _services.GetRequiredService<IHostApplicationLifetime>();
124151

@@ -142,16 +169,18 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
142169
{
143170
_logger.LogInformation("new instance launched");
144171

145-
_ = InitializeServicesAsync(_appLifetime.ApplicationStopping);
146-
147172
// Prevent the TrayWindow from closing, just hide it.
148-
var trayWindow = _services.GetRequiredService<TrayWindow>();
149-
trayWindow.Closed += (_, closedArgs) =>
173+
if (TrayWindow != null)
174+
throw new InvalidOperationException("OnLaunched was called multiple times? TrayWindow is already set");
175+
TrayWindow = _services.GetRequiredService<TrayWindow>();
176+
TrayWindow.Closed += (_, closedArgs) =>
150177
{
151178
if (!_handleWindowClosed) return;
152179
closedArgs.Handled = true;
153-
trayWindow.AppWindow.Hide();
180+
TrayWindow.AppWindow.Hide();
154181
};
182+
183+
_ = InitializeServicesAsync(_appLifetime.ApplicationStopping);
155184
}
156185

157186
/// <summary>
@@ -261,27 +290,49 @@ public void OnActivated(object? sender, AppActivationArguments args)
261290

262291
public void HandleNotification(AppNotificationManager? sender, AppNotificationActivatedEventArgs args)
263292
{
264-
// right now, we don't do anything other than log
265-
_logger.LogInformation("handled notification activation");
293+
_logger.LogInformation("handled notification activation: {Argument}", args.Argument);
294+
_userNotifier.HandleNotificationActivation(args.Arguments);
266295
}
267296

268297
private static void AddDefaultConfig(IConfigurationBuilder builder)
269298
{
270299
var logPath = Path.Combine(
271300
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
272301
"CoderDesktop",
273-
logFilename);
302+
LogFilename);
274303
builder.AddInMemoryCollection(new Dictionary<string, string?>
275304
{
276305
[MutagenControllerConfigSection + ":MutagenExecutablePath"] = @"C:\mutagen.exe",
306+
277307
["Serilog:Using:0"] = "Serilog.Sinks.File",
278-
["Serilog:MinimumLevel"] = "Information",
308+
["Serilog:MinimumLevel"] = DefaultLogLevel,
279309
["Serilog:Enrich:0"] = "FromLogContext",
280310
["Serilog:WriteTo:0:Name"] = "File",
281311
["Serilog:WriteTo:0:Args:path"] = logPath,
282312
["Serilog:WriteTo:0:Args:outputTemplate"] =
283313
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
284314
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",
315+
316+
#if DEBUG
317+
["Serilog:Using:1"] = "Serilog.Sinks.Debug",
318+
["Serilog:Enrich:1"] = "FromLogContext",
319+
["Serilog:WriteTo:1:Name"] = "Debug",
320+
["Serilog:WriteTo:1:Args:outputTemplate"] =
321+
"{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}",
322+
#endif
285323
});
286324
}
325+
326+
public void RunInUiThread(DispatcherQueueHandler action)
327+
{
328+
var dispatcherQueue = TrayWindow?.DispatcherQueue;
329+
if (dispatcherQueue is null)
330+
throw new InvalidOperationException("DispatcherQueue is not available");
331+
if (dispatcherQueue.HasThreadAccess)
332+
{
333+
action();
334+
return;
335+
}
336+
dispatcherQueue.TryEnqueue(action);
337+
}
287338
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy